@shipfox/api-auth 18.0.0 → 20.0.0

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 (151) hide show
  1. package/.turbo/turbo-build.log +17 -2
  2. package/CHANGELOG.md +47 -0
  3. package/README.md +24 -9
  4. package/dist/core/administration.d.ts +15 -0
  5. package/dist/core/administration.d.ts.map +1 -1
  6. package/dist/core/administration.js +44 -2
  7. package/dist/core/administration.js.map +1 -1
  8. package/dist/core/auth.d.ts.map +1 -1
  9. package/dist/core/auth.js +5 -3
  10. package/dist/core/auth.js.map +1 -1
  11. package/dist/core/cimd.d.ts +39 -0
  12. package/dist/core/cimd.d.ts.map +1 -0
  13. package/dist/core/cimd.js +392 -0
  14. package/dist/core/cimd.js.map +1 -0
  15. package/dist/core/entities/agent-access.d.ts +75 -0
  16. package/dist/core/entities/agent-access.d.ts.map +1 -0
  17. package/dist/core/entities/agent-access.js +3 -0
  18. package/dist/core/entities/agent-access.js.map +1 -0
  19. package/dist/core/errors.d.ts +17 -0
  20. package/dist/core/errors.d.ts.map +1 -1
  21. package/dist/core/errors.js +32 -0
  22. package/dist/core/errors.js.map +1 -1
  23. package/dist/core/oauth-client-resolver.d.ts +54 -0
  24. package/dist/core/oauth-client-resolver.d.ts.map +1 -0
  25. package/dist/core/oauth-client-resolver.js +154 -0
  26. package/dist/core/oauth-client-resolver.js.map +1 -0
  27. package/dist/core/oauth-client.d.ts +39 -0
  28. package/dist/core/oauth-client.d.ts.map +1 -0
  29. package/dist/core/oauth-client.js +236 -0
  30. package/dist/core/oauth-client.js.map +1 -0
  31. package/dist/db/admin-user-moderation.d.ts.map +1 -1
  32. package/dist/db/admin-user-moderation.js +30 -26
  33. package/dist/db/admin-user-moderation.js.map +1 -1
  34. package/dist/db/admin-user-summary.d.ts +22 -0
  35. package/dist/db/admin-user-summary.d.ts.map +1 -1
  36. package/dist/db/admin-user-summary.js +75 -1
  37. package/dist/db/admin-user-summary.js.map +1 -1
  38. package/dist/db/admin-users.d.ts +11 -0
  39. package/dist/db/admin-users.d.ts.map +1 -1
  40. package/dist/db/admin-users.js +4 -1
  41. package/dist/db/admin-users.js.map +1 -1
  42. package/dist/db/agent-access-retention.d.ts +10 -0
  43. package/dist/db/agent-access-retention.d.ts.map +1 -0
  44. package/dist/db/agent-access-retention.js +11 -0
  45. package/dist/db/agent-access-retention.js.map +1 -0
  46. package/dist/db/agent-access.d.ts +255 -0
  47. package/dist/db/agent-access.d.ts.map +1 -0
  48. package/dist/db/agent-access.js +705 -0
  49. package/dist/db/agent-access.js.map +1 -0
  50. package/dist/db/db.d.ts +2604 -238
  51. package/dist/db/db.d.ts.map +1 -1
  52. package/dist/db/db.js +7 -0
  53. package/dist/db/db.js.map +1 -1
  54. package/dist/db/schema/agent-access.d.ts +1204 -0
  55. package/dist/db/schema/agent-access.d.ts.map +1 -0
  56. package/dist/db/schema/agent-access.js +267 -0
  57. package/dist/db/schema/agent-access.js.map +1 -0
  58. package/dist/db/schema/users.d.ts.map +1 -1
  59. package/dist/db/schema/users.js +8 -3
  60. package/dist/db/schema/users.js.map +1 -1
  61. package/dist/index.d.ts +11 -2
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +26 -2
  64. package/dist/index.js.map +1 -1
  65. package/dist/metrics/instance.d.ts +1 -1
  66. package/dist/metrics/instance.d.ts.map +1 -1
  67. package/dist/metrics/instance.js.map +1 -1
  68. package/dist/presentation/routes/administration.d.ts.map +1 -1
  69. package/dist/presentation/routes/administration.js +123 -20
  70. package/dist/presentation/routes/administration.js.map +1 -1
  71. package/dist/presentation/routes/email-verification/verify-email-resend.d.ts.map +1 -1
  72. package/dist/presentation/routes/email-verification/verify-email-resend.js +6 -1
  73. package/dist/presentation/routes/email-verification/verify-email-resend.js.map +1 -1
  74. package/dist/presentation/routes/oauth.d.ts +9 -0
  75. package/dist/presentation/routes/oauth.d.ts.map +1 -0
  76. package/dist/presentation/routes/oauth.js +150 -0
  77. package/dist/presentation/routes/oauth.js.map +1 -0
  78. package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
  79. package/dist/presentation/routes/rate-limit.js +34 -0
  80. package/dist/presentation/routes/rate-limit.js.map +1 -1
  81. package/dist/presentation/routes/registration/signup.d.ts.map +1 -1
  82. package/dist/presentation/routes/registration/signup.js +59 -50
  83. package/dist/presentation/routes/registration/signup.js.map +1 -1
  84. package/dist/temporal/activities/agent-access-retention.d.ts +13 -0
  85. package/dist/temporal/activities/agent-access-retention.d.ts.map +1 -0
  86. package/dist/temporal/activities/agent-access-retention.js +58 -0
  87. package/dist/temporal/activities/agent-access-retention.js.map +1 -0
  88. package/dist/temporal/activities/index.d.ts +5 -0
  89. package/dist/temporal/activities/index.d.ts.map +1 -0
  90. package/dist/temporal/activities/index.js +8 -0
  91. package/dist/temporal/activities/index.js.map +1 -0
  92. package/dist/temporal/constants.d.ts +2 -0
  93. package/dist/temporal/constants.d.ts.map +1 -0
  94. package/dist/temporal/constants.js +3 -0
  95. package/dist/temporal/constants.js.map +1 -0
  96. package/dist/temporal/workflows/agent-access-retention-cron.d.ts +2 -0
  97. package/dist/temporal/workflows/agent-access-retention-cron.d.ts.map +1 -0
  98. package/dist/temporal/workflows/agent-access-retention-cron.js +22 -0
  99. package/dist/temporal/workflows/agent-access-retention-cron.js.map +1 -0
  100. package/dist/temporal/workflows/index.bundle.js +25227 -0
  101. package/dist/temporal/workflows/index.bundle.meta.json +1 -0
  102. package/dist/temporal/workflows/index.d.ts +2 -0
  103. package/dist/temporal/workflows/index.d.ts.map +1 -0
  104. package/dist/temporal/workflows/index.js +3 -0
  105. package/dist/temporal/workflows/index.js.map +1 -0
  106. package/dist/tsconfig.test.tsbuildinfo +1 -1
  107. package/drizzle/0003_brainy_luckman.sql +6 -0
  108. package/drizzle/0004_yummy_runaways.sql +110 -0
  109. package/drizzle/meta/0003_snapshot.json +844 -0
  110. package/drizzle/meta/0004_snapshot.json +1751 -0
  111. package/drizzle/meta/_journal.json +14 -0
  112. package/package.json +11 -8
  113. package/src/core/admin-role.test.ts +70 -0
  114. package/src/core/administration.ts +78 -1
  115. package/src/core/auth.ts +3 -5
  116. package/src/core/cimd.test.ts +213 -0
  117. package/src/core/cimd.ts +532 -0
  118. package/src/core/entities/agent-access.ts +80 -0
  119. package/src/core/errors.ts +48 -0
  120. package/src/core/oauth-client-resolver.test.ts +198 -0
  121. package/src/core/oauth-client-resolver.ts +253 -0
  122. package/src/core/oauth-client.test.ts +157 -0
  123. package/src/core/oauth-client.ts +331 -0
  124. package/src/db/admin-user-moderation.ts +53 -45
  125. package/src/db/admin-user-summary.test.ts +228 -0
  126. package/src/db/admin-user-summary.ts +143 -1
  127. package/src/db/admin-users.ts +21 -1
  128. package/src/db/agent-access-retention.ts +10 -0
  129. package/src/db/agent-access.test.ts +1064 -0
  130. package/src/db/agent-access.ts +1491 -0
  131. package/src/db/db.ts +14 -0
  132. package/src/db/schema/agent-access.ts +273 -0
  133. package/src/db/schema/users.ts +9 -3
  134. package/src/index.test.ts +27 -0
  135. package/src/index.ts +82 -0
  136. package/src/metrics/instance.ts +6 -1
  137. package/src/presentation/routes/administration.test.ts +359 -2
  138. package/src/presentation/routes/administration.ts +142 -20
  139. package/src/presentation/routes/email-verification/verify-email-resend.ts +7 -1
  140. package/src/presentation/routes/oauth.test.ts +133 -0
  141. package/src/presentation/routes/oauth.ts +144 -0
  142. package/src/presentation/routes/rate-limit.ts +16 -0
  143. package/src/presentation/routes/registration/signup.ts +52 -41
  144. package/src/temporal/activities/agent-access-retention.ts +76 -0
  145. package/src/temporal/activities/index.ts +5 -0
  146. package/src/temporal/constants.ts +1 -0
  147. package/src/temporal/workflows/agent-access-retention-cron.ts +23 -0
  148. package/src/temporal/workflows/index.ts +1 -0
  149. package/test/globalSetup.ts +1 -1
  150. package/test/routes.ts +3 -1
  151. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,12 +1,12 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { AUTH_USER, adoptAdministrationActorGuard, requireAdministrationActor } from '@shipfox/api-auth-context';
3
- import { adminBootstrapStateSchema, administratorUserLookupQuerySchema, administratorUserMutationResponseSchema, administratorUserSummarySchema, bootstrapAdminOwnerBodySchema, bootstrapAdminOwnerResponseSchema, grantAdminRoleBodySchema, grantAdminRoleResponseSchema, impersonateResponseSchema, impersonateUserBodySchema, listAdminGrantsQuerySchema, listAdminGrantsResponseSchema, reactivateAdministratorUserBodySchema, revokeAdminGrantBodySchema, revokeAdminGrantResponseSchema, revokeAdministratorUserSessionsBodySchema, revokeAdministratorUserSessionsResponseSchema, suspendAdministratorUserBodySchema } from '@shipfox/api-auth-dto';
3
+ import { adminBootstrapStateSchema, administratorUserDirectoryQuerySchema, administratorUserDirectoryResponseSchema, administratorUserLookupQuerySchema, administratorUserMutationResponseSchema, administratorUserSummarySchema, bootstrapAdminOwnerBodySchema, bootstrapAdminOwnerResponseSchema, grantAdminRoleBodySchema, grantAdminRoleResponseSchema, impersonateResponseSchema, impersonateUserBodySchema, listAdminGrantsQuerySchema, listAdminGrantsResponseSchema, reactivateAdministratorUserBodySchema, revokeAdminGrantBodySchema, revokeAdminGrantResponseSchema, revokeAdministratorUserSessionsBodySchema, revokeAdministratorUserSessionsResponseSchema, suspendAdministratorUserBodySchema } from '@shipfox/api-auth-dto';
4
4
  import { decodeTimestampIdCursor, encodeTimestampIdCursor } from '@shipfox/node-drizzle';
5
5
  import { ClientError, defineRoute } from '@shipfox/node-fastify';
6
6
  import { z } from 'zod';
7
7
  import { requireAdminRole } from '#core/admin-role.js';
8
- import { bootstrapFirstAdminOwner, findAdministratorUserSummary, getAdminBootstrapState, grantAdministratorRole, impersonateUser, listAdministratorGrantSummaries, reactivateAdministratorUser, revokeAdministratorGrant, revokeAdministratorUserSessions, suspendAdministratorUser } from '#core/administration.js';
9
- import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, AdminRoleRequiredError, CannotImpersonateAdministratorError, CannotImpersonateSelfError, EmailNotVerifiedError, ImpersonationDisabledError, ImpersonationExpiredError, ImpersonationTargetNotActiveError, InvalidAdminBootstrapTokenError, InvalidCredentialsError, LastAdminOwnerError, UserNotFoundError } from '#core/errors.js';
8
+ import { bootstrapFirstAdminOwner, findAdministratorUserSummary, getAdminBootstrapState, grantAdministratorRole, impersonateUser, listAdministratorGrantSummaries, listAdministratorUsers, reactivateAdministratorUser, revokeAdministratorGrant, revokeAdministratorUserSessions, suspendAdministratorUser } from '#core/administration.js';
9
+ import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, AdminRoleRequiredError, CannotImpersonateAdministratorError, CannotImpersonateSelfError, EmailNotVerifiedError, ImpersonationDisabledError, ImpersonationExpiredError, ImpersonationTargetNotActiveError, InvalidAdminBootstrapTokenError, InvalidAdministratorUserDirectoryFilterError, InvalidCredentialsError, LastAdminOwnerError, UserNotFoundError } from '#core/errors.js';
10
10
  import { getClientContext } from '#presentation/auth/jwt-auth.js';
11
11
  import { toUserDto } from '#presentation/dto/user.js';
12
12
  import { createAuthActorRateLimitPreHandler, createAuthIpRateLimitPreHandler } from './rate-limit.js';
@@ -72,7 +72,50 @@ function toAdministratorUserMutationDto(result) {
72
72
  correlation_id: result.correlationId
73
73
  };
74
74
  }
75
+ function directoryResultCountBucket(count) {
76
+ if (count === 0) return '0';
77
+ if (count <= 10) return '1-10';
78
+ if (count <= 50) return '11-50';
79
+ return '51-100';
80
+ }
81
+ function logAdministratorUserDirectoryRead(params) {
82
+ try {
83
+ params.request.log.info({
84
+ actorId: params.actorId,
85
+ requiredRole: 'admin-observer',
86
+ targetType: 'user-directory',
87
+ requestId: params.request.id,
88
+ result: params.outcome,
89
+ outcome: params.outcome,
90
+ durationMs: params.durationMs,
91
+ resultCountBucket: directoryResultCountBucket(params.resultCount),
92
+ filterPresence: params.filterPresence,
93
+ nextPagePresent: params.nextPagePresent
94
+ }, 'Listed administrator user directory');
95
+ } catch {
96
+ // Logging must not change the directory read outcome.
97
+ }
98
+ }
99
+ function translateImpersonationEligibilityError(error) {
100
+ // The mint primitive re-checks login eligibility on the row it reads, and a
101
+ // concurrent suspension or unverification between the in-transaction ladder
102
+ // read and that read surfaces these errors. Map them to the same documented
103
+ // client error instead of a generic 500.
104
+ if (error instanceof EmailNotVerifiedError || error instanceof InvalidCredentialsError) {
105
+ return new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {
106
+ status: 403
107
+ });
108
+ }
109
+ if (error instanceof ImpersonationExpiredError) {
110
+ return new ClientError('Impersonation session has expired', 'impersonation-expired', {
111
+ status: 410
112
+ });
113
+ }
114
+ return undefined;
115
+ }
75
116
  function translateAdministrationError(error) {
117
+ const impersonationEligibilityError = translateImpersonationEligibilityError(error);
118
+ if (impersonationEligibilityError) throw impersonationEligibilityError;
76
119
  if (error instanceof AdminRoleRequiredError) {
77
120
  throw new ClientError('Administrator role required', 'forbidden', {
78
121
  status: 403,
@@ -81,6 +124,11 @@ function translateAdministrationError(error) {
81
124
  }
82
125
  });
83
126
  }
127
+ if (error instanceof InvalidAdministratorUserDirectoryFilterError) {
128
+ throw new ClientError(error.message, 'validation-error', {
129
+ status: 400
130
+ });
131
+ }
84
132
  if (error instanceof InvalidAdminBootstrapTokenError) {
85
133
  throw new ClientError('Bootstrap token is invalid', 'bootstrap-token-invalid', {
86
134
  status: 403
@@ -136,20 +184,6 @@ function translateAdministrationError(error) {
136
184
  status: 403
137
185
  });
138
186
  }
139
- // The mint primitive re-checks login eligibility (active, verified) on the
140
- // row it reads, and a concurrent suspension or unverification between the
141
- // in-transaction ladder read and that read surfaces these errors. Map them
142
- // to the same documented client error instead of a generic 500.
143
- if (error instanceof EmailNotVerifiedError || error instanceof InvalidCredentialsError) {
144
- throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {
145
- status: 403
146
- });
147
- }
148
- if (error instanceof ImpersonationExpiredError) {
149
- throw new ClientError('Impersonation session has expired', 'impersonation-expired', {
150
- status: 410
151
- });
152
- }
153
187
  throw error;
154
188
  }
155
189
  const bootstrapRoute = defineRoute({
@@ -242,17 +276,85 @@ const userLookupRoute = defineRoute({
242
276
  const { id, user_id: userId, email } = request.query;
243
277
  const lookupId = id ?? userId;
244
278
  const actorId = requireActorId(request);
245
- const user = lookupId ? await findAdministratorUserSummary({
279
+ let user;
280
+ if (lookupId) user = await findAdministratorUserSummary({
246
281
  actorId,
247
282
  id: lookupId
248
- }) : email ? await findAdministratorUserSummary({
283
+ });
284
+ else if (email) user = await findAdministratorUserSummary({
249
285
  actorId,
250
286
  email
251
- }) : undefined;
287
+ });
252
288
  if (!user) throw new UserNotFoundError(lookupId ?? email ?? 'unknown');
253
289
  return toAdministratorUserSummaryDto(user);
254
290
  }
255
291
  });
292
+ const userDirectoryRoute = defineRoute({
293
+ method: 'GET',
294
+ path: '/directory',
295
+ description: 'List bounded administrator-safe user summaries for directory browsing.',
296
+ schema: {
297
+ querystring: administratorUserDirectoryQuerySchema,
298
+ response: {
299
+ 200: administratorUserDirectoryResponseSchema
300
+ }
301
+ },
302
+ preHandler: createAuthActorRateLimitPreHandler('directory'),
303
+ errorHandler: translateAdministrationError,
304
+ handler: async (request)=>{
305
+ const actorId = requireActorId(request);
306
+ const startedAt = performance.now();
307
+ const { search, status, impersonation_eligible: impersonationEligible, limit, cursor } = request.query;
308
+ let resultCount = 0;
309
+ let nextPagePresent = false;
310
+ let outcome = 'failed';
311
+ try {
312
+ const decodedCursor = decodeTimestampIdCursor(cursor);
313
+ if (cursor && !decodedCursor) {
314
+ throw new ClientError('Invalid cursor', 'invalid-cursor', {
315
+ status: 400
316
+ });
317
+ }
318
+ const result = await listAdministratorUsers({
319
+ actorId,
320
+ limit,
321
+ ...decodedCursor ? {
322
+ cursor: decodedCursor
323
+ } : {},
324
+ ...search !== undefined ? {
325
+ search
326
+ } : {},
327
+ ...status !== undefined ? {
328
+ status
329
+ } : {},
330
+ ...impersonationEligible !== undefined ? {
331
+ eligible: impersonationEligible
332
+ } : {}
333
+ });
334
+ resultCount = result.users.length;
335
+ nextPagePresent = result.nextCursor !== null;
336
+ outcome = 'succeeded';
337
+ return {
338
+ users: result.users.map(toAdministratorUserSummaryDto),
339
+ next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null
340
+ };
341
+ } finally{
342
+ logAdministratorUserDirectoryRead({
343
+ request,
344
+ actorId,
345
+ outcome,
346
+ durationMs: Math.round(performance.now() - startedAt),
347
+ resultCount,
348
+ filterPresence: {
349
+ search: search !== undefined,
350
+ status: status !== undefined,
351
+ impersonationEligible: impersonationEligible !== undefined
352
+ },
353
+ nextPagePresent
354
+ });
355
+ }
356
+ }
357
+ });
256
358
  const suspendUserRoute = defineRoute({
257
359
  method: 'POST',
258
360
  path: '/:userId/suspend',
@@ -465,6 +567,7 @@ export function createAdministrationUserRoutes(workspaces) {
465
567
  prefix: '/admin/auth/users',
466
568
  auth: AUTH_USER,
467
569
  routes: [
570
+ userDirectoryRoute,
468
571
  userLookupRoute,
469
572
  suspendUserRoute,
470
573
  reactivateUserRoute,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/presentation/routes/administration.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport {\n AUTH_USER,\n adoptAdministrationActorGuard,\n requireAdministrationActor,\n} from '@shipfox/api-auth-context';\nimport {\n adminBootstrapStateSchema,\n administratorUserLookupQuerySchema,\n administratorUserMutationResponseSchema,\n administratorUserSummarySchema,\n bootstrapAdminOwnerBodySchema,\n bootstrapAdminOwnerResponseSchema,\n grantAdminRoleBodySchema,\n grantAdminRoleResponseSchema,\n impersonateResponseSchema,\n impersonateUserBodySchema,\n listAdminGrantsQuerySchema,\n listAdminGrantsResponseSchema,\n reactivateAdministratorUserBodySchema,\n revokeAdminGrantBodySchema,\n revokeAdminGrantResponseSchema,\n revokeAdministratorUserSessionsBodySchema,\n revokeAdministratorUserSessionsResponseSchema,\n suspendAdministratorUserBodySchema,\n} from '@shipfox/api-auth-dto';\nimport type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';\nimport {decodeTimestampIdCursor, encodeTimestampIdCursor} from '@shipfox/node-drizzle';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {FastifyRequest} from 'fastify';\nimport {z} from 'zod';\nimport {requireAdminRole} from '#core/admin-role.js';\nimport {\n bootstrapFirstAdminOwner,\n findAdministratorUserSummary,\n getAdminBootstrapState,\n grantAdministratorRole,\n impersonateUser,\n listAdministratorGrantSummaries,\n reactivateAdministratorUser,\n revokeAdministratorGrant,\n revokeAdministratorUserSessions,\n suspendAdministratorUser,\n} from '#core/administration.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.js';\nimport type {\n AdministratorGrantSummary,\n AdministratorUserSummary,\n} from '#core/entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n AdminRoleRequiredError,\n CannotImpersonateAdministratorError,\n CannotImpersonateSelfError,\n EmailNotVerifiedError,\n ImpersonationDisabledError,\n ImpersonationExpiredError,\n ImpersonationTargetNotActiveError,\n InvalidAdminBootstrapTokenError,\n InvalidCredentialsError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {getClientContext} from '#presentation/auth/jwt-auth.js';\nimport {toUserDto} from '#presentation/dto/user.js';\nimport {createAuthActorRateLimitPreHandler, createAuthIpRateLimitPreHandler} from './rate-limit.js';\n\nconst idempotencyKeyMaxLength = 256;\n\nfunction requireActorId(request: FastifyRequest): string {\n const client = getClientContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n return client.userId;\n}\n\nfunction requireIdempotencyKey(request: FastifyRequest): string {\n const value = request.headers['idempotency-key'];\n const key = Array.isArray(value) ? value[0] : value;\n if (!key || key.trim().length === 0 || key.length > idempotencyKeyMaxLength) {\n throw new ClientError('Idempotency-Key header is required', 'idempotency-key-required', {\n status: 400,\n });\n }\n return key;\n}\n\nasync function requireAdministratorObserver(request: FastifyRequest): Promise<void> {\n await requireAdminRole({userId: requireActorId(request), minimumRole: 'admin-observer'});\n}\n\nfunction toAdminGrantDto(grant: AdminGrant) {\n return {\n id: grant.id,\n user_id: grant.userId,\n role: grant.role,\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n created_at: grant.createdAt.toISOString(),\n updated_at: grant.updatedAt.toISOString(),\n };\n}\n\nfunction toAdministratorUserSummaryDto(user: AdministratorUserSummary) {\n return {\n id: user.id,\n email: user.email,\n name: user.name,\n status: user.status,\n email_verified_at: user.emailVerifiedAt?.toISOString() ?? null,\n created_at: user.createdAt.toISOString(),\n admin_role: user.adminRole,\n };\n}\n\nfunction toAdministratorGrantSummaryDto(grant: AdministratorGrantSummary) {\n return {\n grant_id: grant.grantId,\n role: grant.role,\n created_at: grant.createdAt.toISOString(),\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n user: grant.user,\n };\n}\n\nfunction toAdministratorUserMutationDto(result: {\n user: AdministratorUserSummary;\n correlationId: string;\n}) {\n return {\n ...toAdministratorUserSummaryDto(result.user),\n correlation_id: result.correlationId,\n };\n}\n\nfunction translateAdministrationError(error: unknown): never {\n if (error instanceof AdminRoleRequiredError) {\n throw new ClientError('Administrator role required', 'forbidden', {\n status: 403,\n details: {required_role: error.minimumRole},\n });\n }\n if (error instanceof InvalidAdminBootstrapTokenError) {\n throw new ClientError('Bootstrap token is invalid', 'bootstrap-token-invalid', {\n status: 403,\n });\n }\n if (error instanceof AdminBootstrapClosedError) {\n throw new ClientError('First administrator owner already exists', 'bootstrap-closed', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantAlreadyExistsError) {\n throw new ClientError('Administrator grant already exists', 'grant-already-exists', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantNotFoundError) {\n throw new ClientError('Administrator grant not found', 'not-found', {status: 404});\n }\n if (error instanceof UserNotFoundError) {\n throw new ClientError('User not found', 'not-found', {status: 404});\n }\n if (error instanceof LastAdminOwnerError) {\n throw new ClientError('Cannot remove the final active administrator owner', 'last-owner', {\n status: 409,\n });\n }\n if (error instanceof AdminIdempotencyKeyReuseError) {\n throw new ClientError(\n 'Idempotency-Key was already used for a different command',\n 'idempotency-key-reused',\n {status: 409},\n );\n }\n if (error instanceof ImpersonationDisabledError) {\n throw new ClientError('Impersonation is disabled', 'impersonation-disabled', {status: 403});\n }\n if (error instanceof CannotImpersonateSelfError) {\n throw new ClientError('Cannot impersonate yourself', 'cannot-impersonate-self', {\n status: 403,\n });\n }\n if (error instanceof CannotImpersonateAdministratorError) {\n throw new ClientError(\n 'Cannot impersonate an administrator',\n 'cannot-impersonate-administrator',\n {status: 403},\n );\n }\n if (error instanceof ImpersonationTargetNotActiveError) {\n throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {\n status: 403,\n });\n }\n // The mint primitive re-checks login eligibility (active, verified) on the\n // row it reads, and a concurrent suspension or unverification between the\n // in-transaction ladder read and that read surfaces these errors. Map them\n // to the same documented client error instead of a generic 500.\n if (error instanceof EmailNotVerifiedError || error instanceof InvalidCredentialsError) {\n throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {\n status: 403,\n });\n }\n if (error instanceof ImpersonationExpiredError) {\n throw new ClientError('Impersonation session has expired', 'impersonation-expired', {\n status: 410,\n });\n }\n throw error;\n}\n\nconst bootstrapRoute = defineRoute({\n method: 'POST',\n path: '/bootstrap',\n description: 'Claim the first administrator owner role with the deployment bootstrap token.',\n schema: {\n body: bootstrapAdminOwnerBodySchema,\n response: {201: bootstrapAdminOwnerResponseSchema},\n },\n preHandler: createAuthIpRateLimitPreHandler('bootstrap'),\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await bootstrapFirstAdminOwner({\n actorId,\n bootstrapToken: request.body.bootstrap_token,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst bootstrapStateRoute = defineRoute({\n method: 'GET',\n path: '/bootstrap-state',\n description: 'Read whether first administrator owner bootstrap is available.',\n schema: {response: {200: adminBootstrapStateSchema}},\n preHandler: createAuthIpRateLimitPreHandler('bootstrap-state'),\n errorHandler: translateAdministrationError,\n handler: async () => ({state: await getAdminBootstrapState()}),\n});\n\nconst listRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'List bounded local administrator grant summaries.',\n schema: {\n querystring: listAdminGrantsQuerySchema,\n response: {200: listAdminGrantsResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {limit, cursor} = request.query;\n const decodedCursor = decodeTimestampIdCursor(cursor);\n if (cursor && !decodedCursor) {\n throw new ClientError('Invalid cursor', 'invalid-cursor', {status: 400});\n }\n\n const result = await listAdministratorGrantSummaries({\n actorId: requireActorId(request),\n limit,\n ...(decodedCursor ? {cursor: decodedCursor} : {}),\n });\n return {\n grants: result.grants.map(toAdministratorGrantSummaryDto),\n next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null,\n };\n },\n});\n\nconst userLookupRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'Find one administrator-safe user summary by exact ID or email.',\n schema: {\n querystring: administratorUserLookupQuerySchema,\n response: {200: administratorUserSummarySchema},\n },\n preHandler: [requireAdministratorObserver, createAuthIpRateLimitPreHandler('lookup')],\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {id, user_id: userId, email} = request.query;\n const lookupId = id ?? userId;\n const actorId = requireActorId(request);\n const user = lookupId\n ? await findAdministratorUserSummary({actorId, id: lookupId})\n : email\n ? await findAdministratorUserSummary({actorId, email})\n : undefined;\n if (!user) throw new UserNotFoundError(lookupId ?? email ?? 'unknown');\n return toAdministratorUserSummaryDto(user);\n },\n});\n\nconst suspendUserRoute = defineRoute({\n method: 'POST',\n path: '/:userId/suspend',\n description: 'Suspend a user and revoke all of its active sessions.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: suspendAdministratorUserBodySchema,\n response: {200: administratorUserMutationResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await suspendAdministratorUser({\n actorId: requireActorId(request),\n userId: request.params.userId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdministratorUserMutationDto(result);\n },\n});\n\nconst reactivateUserRoute = defineRoute({\n method: 'POST',\n path: '/:userId/reactivate',\n description: 'Reactivate a suspended user without restoring revoked sessions.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: reactivateAdministratorUserBodySchema,\n response: {200: administratorUserMutationResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await reactivateAdministratorUser({\n actorId: requireActorId(request),\n userId: request.params.userId,\n ...(request.body.reason ? {reason: request.body.reason} : {}),\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdministratorUserMutationDto(result);\n },\n});\n\nconst revokeUserSessionsRoute = defineRoute({\n method: 'POST',\n path: '/:userId/revoke-sessions',\n description: 'Revoke all active sessions for a user without changing account status.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: revokeAdministratorUserSessionsBodySchema,\n response: {200: revokeAdministratorUserSessionsResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await revokeAdministratorUserSessions({\n actorId: requireActorId(request),\n userId: request.params.userId,\n ...(request.body.reason ? {reason: request.body.reason} : {}),\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return {\n ...toAdministratorUserMutationDto(result),\n sessions_revoked: result.sessionsRevoked,\n };\n },\n});\n\nconst grantRoute = defineRoute({\n method: 'POST',\n path: '/',\n description: 'Grant a local administrator role to an active user.',\n schema: {\n body: grantAdminRoleBodySchema,\n response: {201: grantAdminRoleResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await grantAdministratorRole({\n actorId,\n userId: request.body.user_id,\n role: request.body.role,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst revokeRoute = defineRoute({\n method: 'DELETE',\n path: '/:grantId',\n description: 'Revoke a local administrator grant.',\n schema: {\n params: z.object({grantId: z.string().uuid()}),\n body: revokeAdminGrantBodySchema,\n response: {200: revokeAdminGrantResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const grant = await revokeAdministratorGrant({\n actorId: requireActorId(request),\n grantId: request.params.grantId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdminGrantDto(grant);\n },\n});\n\nexport const administrationRoutes: RouteGroup = adoptAdministrationActorGuard({\n prefix: '/admin/auth/admin-grants',\n auth: AUTH_USER,\n routes: [bootstrapRoute, listRoute, grantRoute, revokeRoute],\n});\n\nexport const administrationBootstrapRoutes: RouteGroup = adoptAdministrationActorGuard({\n prefix: '/admin/auth',\n auth: AUTH_USER,\n routes: [bootstrapStateRoute],\n});\n\nfunction createImpersonateUserRoute(workspaces: WorkspacesInterModuleClient) {\n return defineRoute({\n method: 'POST',\n path: '/:userId/impersonate',\n description:\n 'Mint a short-lived, marked, audited impersonated session for an active, verified, non-administrator user.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: impersonateUserBodySchema,\n response: {200: impersonateResponseSchema},\n },\n // The limiter runs ahead of the impersonated-session rejection: a request\n // carrying an already-issued impersonated token must consume the\n // `impersonate` IP and actor buckets instead of being rejected by the\n // guard first, or marked-session probes would bypass the limiter entirely.\n // The guard still runs before the authorization inside the command.\n preHandler: [\n createAuthActorRateLimitPreHandler('impersonate'),\n (request) => {\n requireAdministrationActor(request);\n return undefined;\n },\n ],\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const client = getClientContext(request);\n const result = await impersonateUser({\n actorId: requireActorId(request),\n ...(client?.impersonatorId ? {actorImpersonatorId: client.impersonatorId} : {}),\n targetUserId: request.params.userId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n // Fastify's default request IDs are process-local counters that reset\n // after a redeploy, so a replay could collide with an earlier mint's\n // ID and suppress the required failure event in the ambiguous-COMMIT\n // reconciliation. The correlation is a fresh process-independent UUID\n // per invocation.\n correlationId: randomUUID(),\n workspaces,\n });\n // `server_time` is the issuer's clock at response time: the banner\n // derives its countdown and Extend availability from this anchor.\n return {\n token: result.token,\n expires_at: result.expiresAt.toISOString(),\n server_time: new Date().toISOString(),\n impersonator_id: result.impersonatorId,\n user: toUserDto(result.user),\n };\n },\n });\n}\n\nexport function createAdministrationUserRoutes(\n workspaces: WorkspacesInterModuleClient,\n): RouteGroup[] {\n return [\n adoptAdministrationActorGuard({\n prefix: '/admin/auth/users',\n auth: AUTH_USER,\n routes: [userLookupRoute, suspendUserRoute, reactivateUserRoute, revokeUserSessionsRoute],\n }),\n // The impersonate route mounts outside the adopted guard on purpose: its\n // limiter must run before the impersonated-session rejection (see\n // `createImpersonateUserRoute`), so the guard is applied positionally\n // there. Every other route under `/admin/auth/users` keeps the adoption.\n {\n prefix: '/admin/auth/users',\n auth: AUTH_USER,\n routes: [createImpersonateUserRoute(workspaces)],\n },\n ];\n}\n"],"names":["randomUUID","AUTH_USER","adoptAdministrationActorGuard","requireAdministrationActor","adminBootstrapStateSchema","administratorUserLookupQuerySchema","administratorUserMutationResponseSchema","administratorUserSummarySchema","bootstrapAdminOwnerBodySchema","bootstrapAdminOwnerResponseSchema","grantAdminRoleBodySchema","grantAdminRoleResponseSchema","impersonateResponseSchema","impersonateUserBodySchema","listAdminGrantsQuerySchema","listAdminGrantsResponseSchema","reactivateAdministratorUserBodySchema","revokeAdminGrantBodySchema","revokeAdminGrantResponseSchema","revokeAdministratorUserSessionsBodySchema","revokeAdministratorUserSessionsResponseSchema","suspendAdministratorUserBodySchema","decodeTimestampIdCursor","encodeTimestampIdCursor","ClientError","defineRoute","z","requireAdminRole","bootstrapFirstAdminOwner","findAdministratorUserSummary","getAdminBootstrapState","grantAdministratorRole","impersonateUser","listAdministratorGrantSummaries","reactivateAdministratorUser","revokeAdministratorGrant","revokeAdministratorUserSessions","suspendAdministratorUser","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","AdminRoleRequiredError","CannotImpersonateAdministratorError","CannotImpersonateSelfError","EmailNotVerifiedError","ImpersonationDisabledError","ImpersonationExpiredError","ImpersonationTargetNotActiveError","InvalidAdminBootstrapTokenError","InvalidCredentialsError","LastAdminOwnerError","UserNotFoundError","getClientContext","toUserDto","createAuthActorRateLimitPreHandler","createAuthIpRateLimitPreHandler","idempotencyKeyMaxLength","requireActorId","request","client","status","userId","requireIdempotencyKey","value","headers","key","Array","isArray","trim","length","requireAdministratorObserver","minimumRole","toAdminGrantDto","grant","id","user_id","role","revoked_at","revokedAt","toISOString","created_at","createdAt","updated_at","updatedAt","toAdministratorUserSummaryDto","user","email","name","email_verified_at","emailVerifiedAt","admin_role","adminRole","toAdministratorGrantSummaryDto","grant_id","grantId","toAdministratorUserMutationDto","result","correlation_id","correlationId","translateAdministrationError","error","details","required_role","bootstrapRoute","method","path","description","schema","body","response","preHandler","errorHandler","handler","reply","actorId","bootstrapToken","bootstrap_token","idempotencyKey","code","bootstrapStateRoute","state","listRoute","querystring","limit","cursor","query","decodedCursor","grants","map","next_cursor","nextCursor","userLookupRoute","lookupId","undefined","suspendUserRoute","params","object","string","uuid","reason","reactivateUserRoute","revokeUserSessionsRoute","sessions_revoked","sessionsRevoked","grantRoute","revokeRoute","administrationRoutes","prefix","auth","routes","administrationBootstrapRoutes","createImpersonateUserRoute","workspaces","impersonatorId","actorImpersonatorId","targetUserId","token","expires_at","expiresAt","server_time","Date","impersonator_id","createAdministrationUserRoutes"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/presentation/routes/administration.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport {\n AUTH_USER,\n adoptAdministrationActorGuard,\n requireAdministrationActor,\n} from '@shipfox/api-auth-context';\nimport {\n adminBootstrapStateSchema,\n administratorUserDirectoryQuerySchema,\n administratorUserDirectoryResponseSchema,\n administratorUserLookupQuerySchema,\n administratorUserMutationResponseSchema,\n administratorUserSummarySchema,\n bootstrapAdminOwnerBodySchema,\n bootstrapAdminOwnerResponseSchema,\n grantAdminRoleBodySchema,\n grantAdminRoleResponseSchema,\n impersonateResponseSchema,\n impersonateUserBodySchema,\n listAdminGrantsQuerySchema,\n listAdminGrantsResponseSchema,\n reactivateAdministratorUserBodySchema,\n revokeAdminGrantBodySchema,\n revokeAdminGrantResponseSchema,\n revokeAdministratorUserSessionsBodySchema,\n revokeAdministratorUserSessionsResponseSchema,\n suspendAdministratorUserBodySchema,\n} from '@shipfox/api-auth-dto';\nimport type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';\nimport {decodeTimestampIdCursor, encodeTimestampIdCursor} from '@shipfox/node-drizzle';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {FastifyRequest} from 'fastify';\nimport {z} from 'zod';\nimport {requireAdminRole} from '#core/admin-role.js';\nimport {\n bootstrapFirstAdminOwner,\n findAdministratorUserSummary,\n getAdminBootstrapState,\n grantAdministratorRole,\n impersonateUser,\n listAdministratorGrantSummaries,\n listAdministratorUsers,\n reactivateAdministratorUser,\n revokeAdministratorGrant,\n revokeAdministratorUserSessions,\n suspendAdministratorUser,\n} from '#core/administration.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.js';\nimport type {\n AdministratorGrantSummary,\n AdministratorUserSummary,\n} from '#core/entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n AdminRoleRequiredError,\n CannotImpersonateAdministratorError,\n CannotImpersonateSelfError,\n EmailNotVerifiedError,\n ImpersonationDisabledError,\n ImpersonationExpiredError,\n ImpersonationTargetNotActiveError,\n InvalidAdminBootstrapTokenError,\n InvalidAdministratorUserDirectoryFilterError,\n InvalidCredentialsError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {getClientContext} from '#presentation/auth/jwt-auth.js';\nimport {toUserDto} from '#presentation/dto/user.js';\nimport {createAuthActorRateLimitPreHandler, createAuthIpRateLimitPreHandler} from './rate-limit.js';\n\nconst idempotencyKeyMaxLength = 256;\n\nfunction requireActorId(request: FastifyRequest): string {\n const client = getClientContext(request);\n if (!client) {\n throw new ClientError('Authentication required', 'unauthorized', {status: 401});\n }\n return client.userId;\n}\n\nfunction requireIdempotencyKey(request: FastifyRequest): string {\n const value = request.headers['idempotency-key'];\n const key = Array.isArray(value) ? value[0] : value;\n if (!key || key.trim().length === 0 || key.length > idempotencyKeyMaxLength) {\n throw new ClientError('Idempotency-Key header is required', 'idempotency-key-required', {\n status: 400,\n });\n }\n return key;\n}\n\nasync function requireAdministratorObserver(request: FastifyRequest): Promise<void> {\n await requireAdminRole({userId: requireActorId(request), minimumRole: 'admin-observer'});\n}\n\nfunction toAdminGrantDto(grant: AdminGrant) {\n return {\n id: grant.id,\n user_id: grant.userId,\n role: grant.role,\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n created_at: grant.createdAt.toISOString(),\n updated_at: grant.updatedAt.toISOString(),\n };\n}\n\nfunction toAdministratorUserSummaryDto(user: AdministratorUserSummary) {\n return {\n id: user.id,\n email: user.email,\n name: user.name,\n status: user.status,\n email_verified_at: user.emailVerifiedAt?.toISOString() ?? null,\n created_at: user.createdAt.toISOString(),\n admin_role: user.adminRole,\n };\n}\n\nfunction toAdministratorGrantSummaryDto(grant: AdministratorGrantSummary) {\n return {\n grant_id: grant.grantId,\n role: grant.role,\n created_at: grant.createdAt.toISOString(),\n revoked_at: grant.revokedAt?.toISOString() ?? null,\n user: grant.user,\n };\n}\n\nfunction toAdministratorUserMutationDto(result: {\n user: AdministratorUserSummary;\n correlationId: string;\n}) {\n return {\n ...toAdministratorUserSummaryDto(result.user),\n correlation_id: result.correlationId,\n };\n}\n\nfunction directoryResultCountBucket(count: number): '0' | '1-10' | '11-50' | '51-100' {\n if (count === 0) return '0';\n if (count <= 10) return '1-10';\n if (count <= 50) return '11-50';\n return '51-100';\n}\n\nfunction logAdministratorUserDirectoryRead(params: {\n request: FastifyRequest;\n actorId: string;\n outcome: 'succeeded' | 'failed';\n durationMs: number;\n resultCount: number;\n filterPresence: {\n search: boolean;\n status: boolean;\n impersonationEligible: boolean;\n };\n nextPagePresent: boolean;\n}): void {\n try {\n params.request.log.info(\n {\n actorId: params.actorId,\n requiredRole: 'admin-observer',\n targetType: 'user-directory',\n requestId: params.request.id,\n result: params.outcome,\n outcome: params.outcome,\n durationMs: params.durationMs,\n resultCountBucket: directoryResultCountBucket(params.resultCount),\n filterPresence: params.filterPresence,\n nextPagePresent: params.nextPagePresent,\n },\n 'Listed administrator user directory',\n );\n } catch {\n // Logging must not change the directory read outcome.\n }\n}\n\nfunction translateImpersonationEligibilityError(error: unknown): ClientError | undefined {\n // The mint primitive re-checks login eligibility on the row it reads, and a\n // concurrent suspension or unverification between the in-transaction ladder\n // read and that read surfaces these errors. Map them to the same documented\n // client error instead of a generic 500.\n if (error instanceof EmailNotVerifiedError || error instanceof InvalidCredentialsError) {\n return new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {\n status: 403,\n });\n }\n if (error instanceof ImpersonationExpiredError) {\n return new ClientError('Impersonation session has expired', 'impersonation-expired', {\n status: 410,\n });\n }\n return undefined;\n}\n\nfunction translateAdministrationError(error: unknown): never {\n const impersonationEligibilityError = translateImpersonationEligibilityError(error);\n if (impersonationEligibilityError) throw impersonationEligibilityError;\n if (error instanceof AdminRoleRequiredError) {\n throw new ClientError('Administrator role required', 'forbidden', {\n status: 403,\n details: {required_role: error.minimumRole},\n });\n }\n if (error instanceof InvalidAdministratorUserDirectoryFilterError) {\n throw new ClientError(error.message, 'validation-error', {status: 400});\n }\n if (error instanceof InvalidAdminBootstrapTokenError) {\n throw new ClientError('Bootstrap token is invalid', 'bootstrap-token-invalid', {\n status: 403,\n });\n }\n if (error instanceof AdminBootstrapClosedError) {\n throw new ClientError('First administrator owner already exists', 'bootstrap-closed', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantAlreadyExistsError) {\n throw new ClientError('Administrator grant already exists', 'grant-already-exists', {\n status: 409,\n });\n }\n if (error instanceof AdminGrantNotFoundError) {\n throw new ClientError('Administrator grant not found', 'not-found', {status: 404});\n }\n if (error instanceof UserNotFoundError) {\n throw new ClientError('User not found', 'not-found', {status: 404});\n }\n if (error instanceof LastAdminOwnerError) {\n throw new ClientError('Cannot remove the final active administrator owner', 'last-owner', {\n status: 409,\n });\n }\n if (error instanceof AdminIdempotencyKeyReuseError) {\n throw new ClientError(\n 'Idempotency-Key was already used for a different command',\n 'idempotency-key-reused',\n {status: 409},\n );\n }\n if (error instanceof ImpersonationDisabledError) {\n throw new ClientError('Impersonation is disabled', 'impersonation-disabled', {status: 403});\n }\n if (error instanceof CannotImpersonateSelfError) {\n throw new ClientError('Cannot impersonate yourself', 'cannot-impersonate-self', {\n status: 403,\n });\n }\n if (error instanceof CannotImpersonateAdministratorError) {\n throw new ClientError(\n 'Cannot impersonate an administrator',\n 'cannot-impersonate-administrator',\n {status: 403},\n );\n }\n if (error instanceof ImpersonationTargetNotActiveError) {\n throw new ClientError('User cannot be impersonated', 'impersonation-target-not-active', {\n status: 403,\n });\n }\n throw error;\n}\n\nconst bootstrapRoute = defineRoute({\n method: 'POST',\n path: '/bootstrap',\n description: 'Claim the first administrator owner role with the deployment bootstrap token.',\n schema: {\n body: bootstrapAdminOwnerBodySchema,\n response: {201: bootstrapAdminOwnerResponseSchema},\n },\n preHandler: createAuthIpRateLimitPreHandler('bootstrap'),\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await bootstrapFirstAdminOwner({\n actorId,\n bootstrapToken: request.body.bootstrap_token,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst bootstrapStateRoute = defineRoute({\n method: 'GET',\n path: '/bootstrap-state',\n description: 'Read whether first administrator owner bootstrap is available.',\n schema: {response: {200: adminBootstrapStateSchema}},\n preHandler: createAuthIpRateLimitPreHandler('bootstrap-state'),\n errorHandler: translateAdministrationError,\n handler: async () => ({state: await getAdminBootstrapState()}),\n});\n\nconst listRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'List bounded local administrator grant summaries.',\n schema: {\n querystring: listAdminGrantsQuerySchema,\n response: {200: listAdminGrantsResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {limit, cursor} = request.query;\n const decodedCursor = decodeTimestampIdCursor(cursor);\n if (cursor && !decodedCursor) {\n throw new ClientError('Invalid cursor', 'invalid-cursor', {status: 400});\n }\n\n const result = await listAdministratorGrantSummaries({\n actorId: requireActorId(request),\n limit,\n ...(decodedCursor ? {cursor: decodedCursor} : {}),\n });\n return {\n grants: result.grants.map(toAdministratorGrantSummaryDto),\n next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null,\n };\n },\n});\n\nconst userLookupRoute = defineRoute({\n method: 'GET',\n path: '/',\n description: 'Find one administrator-safe user summary by exact ID or email.',\n schema: {\n querystring: administratorUserLookupQuerySchema,\n response: {200: administratorUserSummarySchema},\n },\n preHandler: [requireAdministratorObserver, createAuthIpRateLimitPreHandler('lookup')],\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const {id, user_id: userId, email} = request.query;\n const lookupId = id ?? userId;\n const actorId = requireActorId(request);\n let user: Awaited<ReturnType<typeof findAdministratorUserSummary>>;\n if (lookupId) user = await findAdministratorUserSummary({actorId, id: lookupId});\n else if (email) user = await findAdministratorUserSummary({actorId, email});\n if (!user) throw new UserNotFoundError(lookupId ?? email ?? 'unknown');\n return toAdministratorUserSummaryDto(user);\n },\n});\n\nconst userDirectoryRoute = defineRoute({\n method: 'GET',\n path: '/directory',\n description: 'List bounded administrator-safe user summaries for directory browsing.',\n schema: {\n querystring: administratorUserDirectoryQuerySchema,\n response: {200: administratorUserDirectoryResponseSchema},\n },\n preHandler: createAuthActorRateLimitPreHandler('directory'),\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const actorId = requireActorId(request);\n const startedAt = performance.now();\n const {\n search,\n status,\n impersonation_eligible: impersonationEligible,\n limit,\n cursor,\n } = request.query;\n let resultCount = 0;\n let nextPagePresent = false;\n let outcome: 'succeeded' | 'failed' = 'failed';\n\n try {\n const decodedCursor = decodeTimestampIdCursor(cursor);\n if (cursor && !decodedCursor) {\n throw new ClientError('Invalid cursor', 'invalid-cursor', {status: 400});\n }\n\n const result = await listAdministratorUsers({\n actorId,\n limit,\n ...(decodedCursor ? {cursor: decodedCursor} : {}),\n ...(search !== undefined ? {search} : {}),\n ...(status !== undefined ? {status} : {}),\n ...(impersonationEligible !== undefined ? {eligible: impersonationEligible} : {}),\n });\n resultCount = result.users.length;\n nextPagePresent = result.nextCursor !== null;\n outcome = 'succeeded';\n\n return {\n users: result.users.map(toAdministratorUserSummaryDto),\n next_cursor: result.nextCursor ? encodeTimestampIdCursor(result.nextCursor) : null,\n };\n } finally {\n logAdministratorUserDirectoryRead({\n request,\n actorId,\n outcome,\n durationMs: Math.round(performance.now() - startedAt),\n resultCount,\n filterPresence: {\n search: search !== undefined,\n status: status !== undefined,\n impersonationEligible: impersonationEligible !== undefined,\n },\n nextPagePresent,\n });\n }\n },\n});\n\nconst suspendUserRoute = defineRoute({\n method: 'POST',\n path: '/:userId/suspend',\n description: 'Suspend a user and revoke all of its active sessions.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: suspendAdministratorUserBodySchema,\n response: {200: administratorUserMutationResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await suspendAdministratorUser({\n actorId: requireActorId(request),\n userId: request.params.userId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdministratorUserMutationDto(result);\n },\n});\n\nconst reactivateUserRoute = defineRoute({\n method: 'POST',\n path: '/:userId/reactivate',\n description: 'Reactivate a suspended user without restoring revoked sessions.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: reactivateAdministratorUserBodySchema,\n response: {200: administratorUserMutationResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await reactivateAdministratorUser({\n actorId: requireActorId(request),\n userId: request.params.userId,\n ...(request.body.reason ? {reason: request.body.reason} : {}),\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdministratorUserMutationDto(result);\n },\n});\n\nconst revokeUserSessionsRoute = defineRoute({\n method: 'POST',\n path: '/:userId/revoke-sessions',\n description: 'Revoke all active sessions for a user without changing account status.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: revokeAdministratorUserSessionsBodySchema,\n response: {200: revokeAdministratorUserSessionsResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const result = await revokeAdministratorUserSessions({\n actorId: requireActorId(request),\n userId: request.params.userId,\n ...(request.body.reason ? {reason: request.body.reason} : {}),\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return {\n ...toAdministratorUserMutationDto(result),\n sessions_revoked: result.sessionsRevoked,\n };\n },\n});\n\nconst grantRoute = defineRoute({\n method: 'POST',\n path: '/',\n description: 'Grant a local administrator role to an active user.',\n schema: {\n body: grantAdminRoleBodySchema,\n response: {201: grantAdminRoleResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request, reply) => {\n const actorId = requireActorId(request);\n const grant = await grantAdministratorRole({\n actorId,\n userId: request.body.user_id,\n role: request.body.role,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n reply.code(201);\n return toAdminGrantDto(grant);\n },\n});\n\nconst revokeRoute = defineRoute({\n method: 'DELETE',\n path: '/:grantId',\n description: 'Revoke a local administrator grant.',\n schema: {\n params: z.object({grantId: z.string().uuid()}),\n body: revokeAdminGrantBodySchema,\n response: {200: revokeAdminGrantResponseSchema},\n },\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const grant = await revokeAdministratorGrant({\n actorId: requireActorId(request),\n grantId: request.params.grantId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n correlationId: request.id,\n });\n return toAdminGrantDto(grant);\n },\n});\n\nexport const administrationRoutes: RouteGroup = adoptAdministrationActorGuard({\n prefix: '/admin/auth/admin-grants',\n auth: AUTH_USER,\n routes: [bootstrapRoute, listRoute, grantRoute, revokeRoute],\n});\n\nexport const administrationBootstrapRoutes: RouteGroup = adoptAdministrationActorGuard({\n prefix: '/admin/auth',\n auth: AUTH_USER,\n routes: [bootstrapStateRoute],\n});\n\nfunction createImpersonateUserRoute(workspaces: WorkspacesInterModuleClient) {\n return defineRoute({\n method: 'POST',\n path: '/:userId/impersonate',\n description:\n 'Mint a short-lived, marked, audited impersonated session for an active, verified, non-administrator user.',\n schema: {\n params: z.object({userId: z.string().uuid()}),\n body: impersonateUserBodySchema,\n response: {200: impersonateResponseSchema},\n },\n // The limiter runs ahead of the impersonated-session rejection: a request\n // carrying an already-issued impersonated token must consume the\n // `impersonate` IP and actor buckets instead of being rejected by the\n // guard first, or marked-session probes would bypass the limiter entirely.\n // The guard still runs before the authorization inside the command.\n preHandler: [\n createAuthActorRateLimitPreHandler('impersonate'),\n (request) => {\n requireAdministrationActor(request);\n return undefined;\n },\n ],\n errorHandler: translateAdministrationError,\n handler: async (request) => {\n const client = getClientContext(request);\n const result = await impersonateUser({\n actorId: requireActorId(request),\n ...(client?.impersonatorId ? {actorImpersonatorId: client.impersonatorId} : {}),\n targetUserId: request.params.userId,\n reason: request.body.reason,\n idempotencyKey: requireIdempotencyKey(request),\n // Fastify's default request IDs are process-local counters that reset\n // after a redeploy, so a replay could collide with an earlier mint's\n // ID and suppress the required failure event in the ambiguous-COMMIT\n // reconciliation. The correlation is a fresh process-independent UUID\n // per invocation.\n correlationId: randomUUID(),\n workspaces,\n });\n // `server_time` is the issuer's clock at response time: the banner\n // derives its countdown and Extend availability from this anchor.\n return {\n token: result.token,\n expires_at: result.expiresAt.toISOString(),\n server_time: new Date().toISOString(),\n impersonator_id: result.impersonatorId,\n user: toUserDto(result.user),\n };\n },\n });\n}\n\nexport function createAdministrationUserRoutes(\n workspaces: WorkspacesInterModuleClient,\n): RouteGroup[] {\n return [\n adoptAdministrationActorGuard({\n prefix: '/admin/auth/users',\n auth: AUTH_USER,\n routes: [\n userDirectoryRoute,\n userLookupRoute,\n suspendUserRoute,\n reactivateUserRoute,\n revokeUserSessionsRoute,\n ],\n }),\n // The impersonate route mounts outside the adopted guard on purpose: its\n // limiter must run before the impersonated-session rejection (see\n // `createImpersonateUserRoute`), so the guard is applied positionally\n // there. Every other route under `/admin/auth/users` keeps the adoption.\n {\n prefix: '/admin/auth/users',\n auth: AUTH_USER,\n routes: [createImpersonateUserRoute(workspaces)],\n },\n ];\n}\n"],"names":["randomUUID","AUTH_USER","adoptAdministrationActorGuard","requireAdministrationActor","adminBootstrapStateSchema","administratorUserDirectoryQuerySchema","administratorUserDirectoryResponseSchema","administratorUserLookupQuerySchema","administratorUserMutationResponseSchema","administratorUserSummarySchema","bootstrapAdminOwnerBodySchema","bootstrapAdminOwnerResponseSchema","grantAdminRoleBodySchema","grantAdminRoleResponseSchema","impersonateResponseSchema","impersonateUserBodySchema","listAdminGrantsQuerySchema","listAdminGrantsResponseSchema","reactivateAdministratorUserBodySchema","revokeAdminGrantBodySchema","revokeAdminGrantResponseSchema","revokeAdministratorUserSessionsBodySchema","revokeAdministratorUserSessionsResponseSchema","suspendAdministratorUserBodySchema","decodeTimestampIdCursor","encodeTimestampIdCursor","ClientError","defineRoute","z","requireAdminRole","bootstrapFirstAdminOwner","findAdministratorUserSummary","getAdminBootstrapState","grantAdministratorRole","impersonateUser","listAdministratorGrantSummaries","listAdministratorUsers","reactivateAdministratorUser","revokeAdministratorGrant","revokeAdministratorUserSessions","suspendAdministratorUser","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","AdminRoleRequiredError","CannotImpersonateAdministratorError","CannotImpersonateSelfError","EmailNotVerifiedError","ImpersonationDisabledError","ImpersonationExpiredError","ImpersonationTargetNotActiveError","InvalidAdminBootstrapTokenError","InvalidAdministratorUserDirectoryFilterError","InvalidCredentialsError","LastAdminOwnerError","UserNotFoundError","getClientContext","toUserDto","createAuthActorRateLimitPreHandler","createAuthIpRateLimitPreHandler","idempotencyKeyMaxLength","requireActorId","request","client","status","userId","requireIdempotencyKey","value","headers","key","Array","isArray","trim","length","requireAdministratorObserver","minimumRole","toAdminGrantDto","grant","id","user_id","role","revoked_at","revokedAt","toISOString","created_at","createdAt","updated_at","updatedAt","toAdministratorUserSummaryDto","user","email","name","email_verified_at","emailVerifiedAt","admin_role","adminRole","toAdministratorGrantSummaryDto","grant_id","grantId","toAdministratorUserMutationDto","result","correlation_id","correlationId","directoryResultCountBucket","count","logAdministratorUserDirectoryRead","params","log","info","actorId","requiredRole","targetType","requestId","outcome","durationMs","resultCountBucket","resultCount","filterPresence","nextPagePresent","translateImpersonationEligibilityError","error","undefined","translateAdministrationError","impersonationEligibilityError","details","required_role","message","bootstrapRoute","method","path","description","schema","body","response","preHandler","errorHandler","handler","reply","bootstrapToken","bootstrap_token","idempotencyKey","code","bootstrapStateRoute","state","listRoute","querystring","limit","cursor","query","decodedCursor","grants","map","next_cursor","nextCursor","userLookupRoute","lookupId","userDirectoryRoute","startedAt","performance","now","search","impersonation_eligible","impersonationEligible","eligible","users","Math","round","suspendUserRoute","object","string","uuid","reason","reactivateUserRoute","revokeUserSessionsRoute","sessions_revoked","sessionsRevoked","grantRoute","revokeRoute","administrationRoutes","prefix","auth","routes","administrationBootstrapRoutes","createImpersonateUserRoute","workspaces","impersonatorId","actorImpersonatorId","targetUserId","token","expires_at","expiresAt","server_time","Date","impersonator_id","createAdministrationUserRoutes"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"verify-email-resend.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/email-verification/verify-email-resend.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,sBAAsB,iDAiCjC,CAAC"}
1
+ {"version":3,"file":"verify-email-resend.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/email-verification/verify-email-resend.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,sBAAsB,iDAiCjC,CAAC"}
@@ -3,6 +3,11 @@ import { EmailChallengeError } from '@shipfox/api-email-challenges';
3
3
  import { ClientError, defineRoute } from '@shipfox/node-fastify';
4
4
  import { resendEmailVerification } from '#core/auth.js';
5
5
  import { createAuthRateLimitPreHandler } from '#presentation/routes/rate-limit.js';
6
+ function emailChallengeStatus(code) {
7
+ if (code === 'limited') return 429;
8
+ if (code === 'expired') return 410;
9
+ return 400;
10
+ }
6
11
  export const verifyEmailResendRoute = defineRoute({
7
12
  method: 'POST',
8
13
  path: '/verify-email/resend',
@@ -17,7 +22,7 @@ export const verifyEmailResendRoute = defineRoute({
17
22
  errorHandler: (error)=>{
18
23
  if (error instanceof EmailChallengeError) {
19
24
  throw new ClientError(error.message, `email-challenge-${error.code}`, {
20
- status: error.code === 'limited' ? 429 : error.code === 'expired' ? 410 : 400,
25
+ status: emailChallengeStatus(error.code),
21
26
  ...error.retryAt ? {
22
27
  details: {
23
28
  retry_at: error.retryAt.toISOString()
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/presentation/routes/email-verification/verify-email-resend.ts"],"sourcesContent":["import {verifyEmailResendBodySchema, verifyEmailResendResponseSchema} from '@shipfox/api-auth-dto';\nimport {EmailChallengeError} from '@shipfox/api-email-challenges';\nimport {ClientError, defineRoute} from '@shipfox/node-fastify';\nimport {resendEmailVerification} from '#core/auth.js';\nimport {createAuthRateLimitPreHandler} from '#presentation/routes/rate-limit.js';\n\nexport const verifyEmailResendRoute = defineRoute({\n method: 'POST',\n path: '/verify-email/resend',\n description: 'Send a new email verification code if the account can be verified.',\n schema: {\n body: verifyEmailResendBodySchema,\n response: {\n 200: verifyEmailResendResponseSchema,\n },\n },\n preHandler: createAuthRateLimitPreHandler('email-send'),\n errorHandler: (error) => {\n if (error instanceof EmailChallengeError) {\n throw new ClientError(error.message, `email-challenge-${error.code}`, {\n status: error.code === 'limited' ? 429 : error.code === 'expired' ? 410 : 400,\n ...(error.retryAt ? {details: {retry_at: error.retryAt.toISOString()}} : {}),\n });\n }\n throw error;\n },\n handler: async (request, reply) => {\n const {email, challenge_id} = request.body;\n\n const result = await resendEmailVerification({\n email,\n challengeId: challenge_id,\n sourceIp: request.ip,\n });\n\n reply.code(200).send({\n next_resend_available_at: result.nextResendAvailableAt.toISOString(),\n });\n },\n});\n"],"names":["verifyEmailResendBodySchema","verifyEmailResendResponseSchema","EmailChallengeError","ClientError","defineRoute","resendEmailVerification","createAuthRateLimitPreHandler","verifyEmailResendRoute","method","path","description","schema","body","response","preHandler","errorHandler","error","message","code","status","retryAt","details","retry_at","toISOString","handler","request","reply","email","challenge_id","result","challengeId","sourceIp","ip","send","next_resend_available_at","nextResendAvailableAt"],"mappings":"AAAA,SAAQA,2BAA2B,EAAEC,+BAA+B,QAAO,wBAAwB;AACnG,SAAQC,mBAAmB,QAAO,gCAAgC;AAClE,SAAQC,WAAW,EAAEC,WAAW,QAAO,wBAAwB;AAC/D,SAAQC,uBAAuB,QAAO,gBAAgB;AACtD,SAAQC,6BAA6B,QAAO,qCAAqC;AAEjF,OAAO,MAAMC,yBAAyBH,YAAY;IAChDI,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNC,MAAMZ;QACNa,UAAU;YACR,KAAKZ;QACP;IACF;IACAa,YAAYR,8BAA8B;IAC1CS,cAAc,CAACC;QACb,IAAIA,iBAAiBd,qBAAqB;YACxC,MAAM,IAAIC,YAAYa,MAAMC,OAAO,EAAE,CAAC,gBAAgB,EAAED,MAAME,IAAI,EAAE,EAAE;gBACpEC,QAAQH,MAAME,IAAI,KAAK,YAAY,MAAMF,MAAME,IAAI,KAAK,YAAY,MAAM;gBAC1E,GAAIF,MAAMI,OAAO,GAAG;oBAACC,SAAS;wBAACC,UAAUN,MAAMI,OAAO,CAACG,WAAW;oBAAE;gBAAC,IAAI,CAAC,CAAC;YAC7E;QACF;QACA,MAAMP;IACR;IACAQ,SAAS,OAAOC,SAASC;QACvB,MAAM,EAACC,KAAK,EAAEC,YAAY,EAAC,GAAGH,QAAQb,IAAI;QAE1C,MAAMiB,SAAS,MAAMxB,wBAAwB;YAC3CsB;YACAG,aAAaF;YACbG,UAAUN,QAAQO,EAAE;QACtB;QAEAN,MAAMR,IAAI,CAAC,KAAKe,IAAI,CAAC;YACnBC,0BAA0BL,OAAOM,qBAAqB,CAACZ,WAAW;QACpE;IACF;AACF,GAAG"}
1
+ {"version":3,"sources":["../../../../src/presentation/routes/email-verification/verify-email-resend.ts"],"sourcesContent":["import {verifyEmailResendBodySchema, verifyEmailResendResponseSchema} from '@shipfox/api-auth-dto';\nimport {EmailChallengeError} from '@shipfox/api-email-challenges';\nimport {ClientError, defineRoute} from '@shipfox/node-fastify';\nimport {resendEmailVerification} from '#core/auth.js';\nimport {createAuthRateLimitPreHandler} from '#presentation/routes/rate-limit.js';\n\nfunction emailChallengeStatus(code: EmailChallengeError['code']): 400 | 410 | 429 {\n if (code === 'limited') return 429;\n if (code === 'expired') return 410;\n return 400;\n}\n\nexport const verifyEmailResendRoute = defineRoute({\n method: 'POST',\n path: '/verify-email/resend',\n description: 'Send a new email verification code if the account can be verified.',\n schema: {\n body: verifyEmailResendBodySchema,\n response: {\n 200: verifyEmailResendResponseSchema,\n },\n },\n preHandler: createAuthRateLimitPreHandler('email-send'),\n errorHandler: (error) => {\n if (error instanceof EmailChallengeError) {\n throw new ClientError(error.message, `email-challenge-${error.code}`, {\n status: emailChallengeStatus(error.code),\n ...(error.retryAt ? {details: {retry_at: error.retryAt.toISOString()}} : {}),\n });\n }\n throw error;\n },\n handler: async (request, reply) => {\n const {email, challenge_id} = request.body;\n\n const result = await resendEmailVerification({\n email,\n challengeId: challenge_id,\n sourceIp: request.ip,\n });\n\n reply.code(200).send({\n next_resend_available_at: result.nextResendAvailableAt.toISOString(),\n });\n },\n});\n"],"names":["verifyEmailResendBodySchema","verifyEmailResendResponseSchema","EmailChallengeError","ClientError","defineRoute","resendEmailVerification","createAuthRateLimitPreHandler","emailChallengeStatus","code","verifyEmailResendRoute","method","path","description","schema","body","response","preHandler","errorHandler","error","message","status","retryAt","details","retry_at","toISOString","handler","request","reply","email","challenge_id","result","challengeId","sourceIp","ip","send","next_resend_available_at","nextResendAvailableAt"],"mappings":"AAAA,SAAQA,2BAA2B,EAAEC,+BAA+B,QAAO,wBAAwB;AACnG,SAAQC,mBAAmB,QAAO,gCAAgC;AAClE,SAAQC,WAAW,EAAEC,WAAW,QAAO,wBAAwB;AAC/D,SAAQC,uBAAuB,QAAO,gBAAgB;AACtD,SAAQC,6BAA6B,QAAO,qCAAqC;AAEjF,SAASC,qBAAqBC,IAAiC;IAC7D,IAAIA,SAAS,WAAW,OAAO;IAC/B,IAAIA,SAAS,WAAW,OAAO;IAC/B,OAAO;AACT;AAEA,OAAO,MAAMC,yBAAyBL,YAAY;IAChDM,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNC,MAAMd;QACNe,UAAU;YACR,KAAKd;QACP;IACF;IACAe,YAAYV,8BAA8B;IAC1CW,cAAc,CAACC;QACb,IAAIA,iBAAiBhB,qBAAqB;YACxC,MAAM,IAAIC,YAAYe,MAAMC,OAAO,EAAE,CAAC,gBAAgB,EAAED,MAAMV,IAAI,EAAE,EAAE;gBACpEY,QAAQb,qBAAqBW,MAAMV,IAAI;gBACvC,GAAIU,MAAMG,OAAO,GAAG;oBAACC,SAAS;wBAACC,UAAUL,MAAMG,OAAO,CAACG,WAAW;oBAAE;gBAAC,IAAI,CAAC,CAAC;YAC7E;QACF;QACA,MAAMN;IACR;IACAO,SAAS,OAAOC,SAASC;QACvB,MAAM,EAACC,KAAK,EAAEC,YAAY,EAAC,GAAGH,QAAQZ,IAAI;QAE1C,MAAMgB,SAAS,MAAMzB,wBAAwB;YAC3CuB;YACAG,aAAaF;YACbG,UAAUN,QAAQO,EAAE;QACtB;QAEAN,MAAMnB,IAAI,CAAC,KAAK0B,IAAI,CAAC;YACnBC,0BAA0BL,OAAOM,qBAAqB,CAACZ,WAAW;QACpE;IACF;AACF,GAAG"}
@@ -0,0 +1,9 @@
1
+ import { type RouteGroup } from '@shipfox/node-fastify';
2
+ export interface CreateOAuthRoutesOptions {
3
+ /** Validated here and deliberately injected until production composition. */
4
+ apiPublicUrl: string;
5
+ }
6
+ export declare function createOAuthMetadataRoutes(options: CreateOAuthRoutesOptions): RouteGroup;
7
+ export declare function createOAuthClientIdentificationRoutes(): RouteGroup;
8
+ export declare function createOAuthRoutes(options: CreateOAuthRoutesOptions): RouteGroup;
9
+ //# sourceMappingURL=oauth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/oauth.ts"],"names":[],"mappings":"AAOA,OAAO,EAA2B,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AAWhF,MAAM,WAAW,wBAAwB;IACvC,6EAA6E;IAC7E,YAAY,EAAE,MAAM,CAAC;CACtB;AAoGD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,wBAAwB,GAAG,UAAU,CASvF;AAED,wBAAgB,qCAAqC,IAAI,UAAU,CAElE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,UAAU,CAO/E"}
@@ -0,0 +1,150 @@
1
+ import { oauthAuthorizationServerMetadataSchema, oauthDynamicClientRegistrationRequestSchema, oauthDynamicClientRegistrationResponseSchema, oauthProtectedResourceMetadataSchema } from '@shipfox/api-auth-dto';
2
+ import { ClientError, defineRoute } from '@shipfox/node-fastify';
3
+ import { InvalidOAuthClientMetadataError, InvalidOAuthConfigurationError, OAuthMetadataFetchError, OAuthRedirectUriNotRegisteredError } from '#core/errors.js';
4
+ import { validateOAuthPublicOrigin } from '#core/oauth-client.js';
5
+ import { registerOAuthClient } from '#core/oauth-client-resolver.js';
6
+ import { createAuthIpRateLimitPreHandler } from './rate-limit.js';
7
+ function apiPublicOrigin(options) {
8
+ return validateOAuthPublicOrigin(options.apiPublicUrl);
9
+ }
10
+ function translateOAuthError(error) {
11
+ if (error instanceof InvalidOAuthClientMetadataError || error instanceof OAuthMetadataFetchError || error instanceof OAuthRedirectUriNotRegisteredError) {
12
+ throw new ClientError('OAuth client metadata is invalid', 'invalid-client-metadata', {
13
+ status: 400
14
+ });
15
+ }
16
+ if (error instanceof InvalidOAuthConfigurationError) {
17
+ throw new ClientError('OAuth configuration is invalid', 'oauth-configuration-invalid', {
18
+ status: 500
19
+ });
20
+ }
21
+ throw error;
22
+ }
23
+ function registrationResponse(result) {
24
+ return {
25
+ client_id: result.metadata.clientId,
26
+ client_name: result.metadata.clientName,
27
+ redirect_uris: result.metadata.redirectUris,
28
+ grant_types: result.metadata.grantTypes,
29
+ response_types: result.metadata.responseTypes,
30
+ token_endpoint_auth_method: result.metadata.tokenEndpointAuthMethod,
31
+ scope: result.metadata.scope
32
+ };
33
+ }
34
+ function createProtectedResourceMetadataRoute(origin) {
35
+ return defineRoute({
36
+ method: 'GET',
37
+ path: '/.well-known/oauth-protected-resource',
38
+ description: 'Describe the protected MCP resource and its authorization server.',
39
+ schema: {
40
+ response: {
41
+ 200: oauthProtectedResourceMetadataSchema
42
+ }
43
+ },
44
+ handler: ()=>({
45
+ resource: `${origin}/mcp`,
46
+ authorization_servers: [
47
+ origin
48
+ ],
49
+ scopes_supported: [
50
+ 'read'
51
+ ]
52
+ })
53
+ });
54
+ }
55
+ function createAuthorizationServerMetadataRoute(origin) {
56
+ return defineRoute({
57
+ method: 'GET',
58
+ path: '/.well-known/oauth-authorization-server',
59
+ description: 'Describe the MCP OAuth authorization server.',
60
+ schema: {
61
+ response: {
62
+ 200: oauthAuthorizationServerMetadataSchema
63
+ }
64
+ },
65
+ handler: ()=>({
66
+ issuer: origin,
67
+ authorization_endpoint: `${origin}/oauth/authorize`,
68
+ token_endpoint: `${origin}/oauth/token`,
69
+ registration_endpoint: `${origin}/oauth/register`,
70
+ response_types_supported: [
71
+ 'code'
72
+ ],
73
+ grant_types_supported: [
74
+ 'authorization_code',
75
+ 'refresh_token'
76
+ ],
77
+ code_challenge_methods_supported: [
78
+ 'S256'
79
+ ],
80
+ token_endpoint_auth_methods_supported: [
81
+ 'none'
82
+ ],
83
+ scopes_supported: [
84
+ 'read'
85
+ ],
86
+ client_id_metadata_document_supported: true
87
+ })
88
+ });
89
+ }
90
+ function createDynamicRegistrationRoute() {
91
+ return defineRoute({
92
+ method: 'POST',
93
+ path: '/oauth/register',
94
+ description: 'Register a public OAuth client for MCP access.',
95
+ options: {
96
+ bodyLimit: 32 * 1024
97
+ },
98
+ schema: {
99
+ body: oauthDynamicClientRegistrationRequestSchema,
100
+ response: {
101
+ 201: oauthDynamicClientRegistrationResponseSchema
102
+ }
103
+ },
104
+ preHandler: createAuthIpRateLimitPreHandler('oauth-register'),
105
+ errorHandler: translateOAuthError,
106
+ handler: async (request, reply)=>{
107
+ const result = await registerOAuthClient({
108
+ clientName: request.body.client_name,
109
+ redirectUris: request.body.redirect_uris,
110
+ grantTypes: request.body.grant_types,
111
+ responseTypes: request.body.response_types,
112
+ tokenEndpointAuthMethod: request.body.token_endpoint_auth_method,
113
+ scope: request.body.scope
114
+ });
115
+ reply.code(201);
116
+ return registrationResponse(result);
117
+ }
118
+ });
119
+ }
120
+ export function createOAuthMetadataRoutes(options) {
121
+ const origin = apiPublicOrigin(options);
122
+ return {
123
+ prefix: '',
124
+ routes: [
125
+ createProtectedResourceMetadataRoute(origin),
126
+ createAuthorizationServerMetadataRoute(origin)
127
+ ]
128
+ };
129
+ }
130
+ export function createOAuthClientIdentificationRoutes() {
131
+ return {
132
+ prefix: '',
133
+ routes: [
134
+ createDynamicRegistrationRoute()
135
+ ]
136
+ };
137
+ }
138
+ export function createOAuthRoutes(options) {
139
+ const metadataRoutes = createOAuthMetadataRoutes(options);
140
+ const clientIdentificationRoutes = createOAuthClientIdentificationRoutes();
141
+ return {
142
+ prefix: '',
143
+ routes: [
144
+ ...metadataRoutes.routes,
145
+ ...clientIdentificationRoutes.routes
146
+ ]
147
+ };
148
+ }
149
+
150
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/presentation/routes/oauth.ts"],"sourcesContent":["import {\n type OAuthDynamicClientRegistrationResponseDto,\n oauthAuthorizationServerMetadataSchema,\n oauthDynamicClientRegistrationRequestSchema,\n oauthDynamicClientRegistrationResponseSchema,\n oauthProtectedResourceMetadataSchema,\n} from '@shipfox/api-auth-dto';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport {\n InvalidOAuthClientMetadataError,\n InvalidOAuthConfigurationError,\n OAuthMetadataFetchError,\n OAuthRedirectUriNotRegisteredError,\n} from '#core/errors.js';\nimport {validateOAuthPublicOrigin} from '#core/oauth-client.js';\nimport {registerOAuthClient} from '#core/oauth-client-resolver.js';\nimport {createAuthIpRateLimitPreHandler} from './rate-limit.js';\n\nexport interface CreateOAuthRoutesOptions {\n /** Validated here and deliberately injected until production composition. */\n apiPublicUrl: string;\n}\n\nfunction apiPublicOrigin(options: CreateOAuthRoutesOptions): string {\n return validateOAuthPublicOrigin(options.apiPublicUrl);\n}\n\nfunction translateOAuthError(error: unknown): never {\n if (\n error instanceof InvalidOAuthClientMetadataError ||\n error instanceof OAuthMetadataFetchError ||\n error instanceof OAuthRedirectUriNotRegisteredError\n ) {\n throw new ClientError('OAuth client metadata is invalid', 'invalid-client-metadata', {\n status: 400,\n });\n }\n if (error instanceof InvalidOAuthConfigurationError) {\n throw new ClientError('OAuth configuration is invalid', 'oauth-configuration-invalid', {\n status: 500,\n });\n }\n throw error;\n}\n\nfunction registrationResponse(\n result: Awaited<ReturnType<typeof registerOAuthClient>>,\n): OAuthDynamicClientRegistrationResponseDto {\n return {\n client_id: result.metadata.clientId,\n client_name: result.metadata.clientName,\n redirect_uris: result.metadata.redirectUris,\n grant_types: result.metadata.grantTypes,\n response_types: result.metadata.responseTypes,\n token_endpoint_auth_method: result.metadata.tokenEndpointAuthMethod,\n scope: result.metadata.scope,\n };\n}\n\nfunction createProtectedResourceMetadataRoute(origin: string) {\n return defineRoute({\n method: 'GET',\n path: '/.well-known/oauth-protected-resource',\n description: 'Describe the protected MCP resource and its authorization server.',\n schema: {response: {200: oauthProtectedResourceMetadataSchema}},\n handler: () => ({\n resource: `${origin}/mcp`,\n authorization_servers: [origin],\n scopes_supported: ['read'],\n }),\n });\n}\n\nfunction createAuthorizationServerMetadataRoute(origin: string) {\n return defineRoute({\n method: 'GET',\n path: '/.well-known/oauth-authorization-server',\n description: 'Describe the MCP OAuth authorization server.',\n schema: {response: {200: oauthAuthorizationServerMetadataSchema}},\n handler: () => ({\n issuer: origin,\n authorization_endpoint: `${origin}/oauth/authorize`,\n token_endpoint: `${origin}/oauth/token`,\n registration_endpoint: `${origin}/oauth/register`,\n response_types_supported: ['code'],\n grant_types_supported: ['authorization_code', 'refresh_token'],\n code_challenge_methods_supported: ['S256'],\n token_endpoint_auth_methods_supported: ['none'],\n scopes_supported: ['read'],\n client_id_metadata_document_supported: true,\n }),\n });\n}\n\nfunction createDynamicRegistrationRoute() {\n return defineRoute({\n method: 'POST',\n path: '/oauth/register',\n description: 'Register a public OAuth client for MCP access.',\n options: {bodyLimit: 32 * 1024},\n schema: {\n body: oauthDynamicClientRegistrationRequestSchema,\n response: {201: oauthDynamicClientRegistrationResponseSchema},\n },\n preHandler: createAuthIpRateLimitPreHandler('oauth-register'),\n errorHandler: translateOAuthError,\n handler: async (request, reply) => {\n const result = await registerOAuthClient({\n clientName: request.body.client_name,\n redirectUris: request.body.redirect_uris,\n grantTypes: request.body.grant_types,\n responseTypes: request.body.response_types,\n tokenEndpointAuthMethod: request.body.token_endpoint_auth_method,\n scope: request.body.scope,\n });\n reply.code(201);\n return registrationResponse(result);\n },\n });\n}\n\nexport function createOAuthMetadataRoutes(options: CreateOAuthRoutesOptions): RouteGroup {\n const origin = apiPublicOrigin(options);\n return {\n prefix: '',\n routes: [\n createProtectedResourceMetadataRoute(origin),\n createAuthorizationServerMetadataRoute(origin),\n ],\n };\n}\n\nexport function createOAuthClientIdentificationRoutes(): RouteGroup {\n return {prefix: '', routes: [createDynamicRegistrationRoute()]};\n}\n\nexport function createOAuthRoutes(options: CreateOAuthRoutesOptions): RouteGroup {\n const metadataRoutes = createOAuthMetadataRoutes(options);\n const clientIdentificationRoutes = createOAuthClientIdentificationRoutes();\n return {\n prefix: '',\n routes: [...metadataRoutes.routes, ...clientIdentificationRoutes.routes],\n };\n}\n"],"names":["oauthAuthorizationServerMetadataSchema","oauthDynamicClientRegistrationRequestSchema","oauthDynamicClientRegistrationResponseSchema","oauthProtectedResourceMetadataSchema","ClientError","defineRoute","InvalidOAuthClientMetadataError","InvalidOAuthConfigurationError","OAuthMetadataFetchError","OAuthRedirectUriNotRegisteredError","validateOAuthPublicOrigin","registerOAuthClient","createAuthIpRateLimitPreHandler","apiPublicOrigin","options","apiPublicUrl","translateOAuthError","error","status","registrationResponse","result","client_id","metadata","clientId","client_name","clientName","redirect_uris","redirectUris","grant_types","grantTypes","response_types","responseTypes","token_endpoint_auth_method","tokenEndpointAuthMethod","scope","createProtectedResourceMetadataRoute","origin","method","path","description","schema","response","handler","resource","authorization_servers","scopes_supported","createAuthorizationServerMetadataRoute","issuer","authorization_endpoint","token_endpoint","registration_endpoint","response_types_supported","grant_types_supported","code_challenge_methods_supported","token_endpoint_auth_methods_supported","client_id_metadata_document_supported","createDynamicRegistrationRoute","bodyLimit","body","preHandler","errorHandler","request","reply","code","createOAuthMetadataRoutes","prefix","routes","createOAuthClientIdentificationRoutes","createOAuthRoutes","metadataRoutes","clientIdentificationRoutes"],"mappings":"AAAA,SAEEA,sCAAsC,EACtCC,2CAA2C,EAC3CC,4CAA4C,EAC5CC,oCAAoC,QAC/B,wBAAwB;AAC/B,SAAQC,WAAW,EAAEC,WAAW,QAAwB,wBAAwB;AAChF,SACEC,+BAA+B,EAC/BC,8BAA8B,EAC9BC,uBAAuB,EACvBC,kCAAkC,QAC7B,kBAAkB;AACzB,SAAQC,yBAAyB,QAAO,wBAAwB;AAChE,SAAQC,mBAAmB,QAAO,iCAAiC;AACnE,SAAQC,+BAA+B,QAAO,kBAAkB;AAOhE,SAASC,gBAAgBC,OAAiC;IACxD,OAAOJ,0BAA0BI,QAAQC,YAAY;AACvD;AAEA,SAASC,oBAAoBC,KAAc;IACzC,IACEA,iBAAiBX,mCACjBW,iBAAiBT,2BACjBS,iBAAiBR,oCACjB;QACA,MAAM,IAAIL,YAAY,oCAAoC,2BAA2B;YACnFc,QAAQ;QACV;IACF;IACA,IAAID,iBAAiBV,gCAAgC;QACnD,MAAM,IAAIH,YAAY,kCAAkC,+BAA+B;YACrFc,QAAQ;QACV;IACF;IACA,MAAMD;AACR;AAEA,SAASE,qBACPC,MAAuD;IAEvD,OAAO;QACLC,WAAWD,OAAOE,QAAQ,CAACC,QAAQ;QACnCC,aAAaJ,OAAOE,QAAQ,CAACG,UAAU;QACvCC,eAAeN,OAAOE,QAAQ,CAACK,YAAY;QAC3CC,aAAaR,OAAOE,QAAQ,CAACO,UAAU;QACvCC,gBAAgBV,OAAOE,QAAQ,CAACS,aAAa;QAC7CC,4BAA4BZ,OAAOE,QAAQ,CAACW,uBAAuB;QACnEC,OAAOd,OAAOE,QAAQ,CAACY,KAAK;IAC9B;AACF;AAEA,SAASC,qCAAqCC,MAAc;IAC1D,OAAO/B,YAAY;QACjBgC,QAAQ;QACRC,MAAM;QACNC,aAAa;QACbC,QAAQ;YAACC,UAAU;gBAAC,KAAKtC;YAAoC;QAAC;QAC9DuC,SAAS,IAAO,CAAA;gBACdC,UAAU,GAAGP,OAAO,IAAI,CAAC;gBACzBQ,uBAAuB;oBAACR;iBAAO;gBAC/BS,kBAAkB;oBAAC;iBAAO;YAC5B,CAAA;IACF;AACF;AAEA,SAASC,uCAAuCV,MAAc;IAC5D,OAAO/B,YAAY;QACjBgC,QAAQ;QACRC,MAAM;QACNC,aAAa;QACbC,QAAQ;YAACC,UAAU;gBAAC,KAAKzC;YAAsC;QAAC;QAChE0C,SAAS,IAAO,CAAA;gBACdK,QAAQX;gBACRY,wBAAwB,GAAGZ,OAAO,gBAAgB,CAAC;gBACnDa,gBAAgB,GAAGb,OAAO,YAAY,CAAC;gBACvCc,uBAAuB,GAAGd,OAAO,eAAe,CAAC;gBACjDe,0BAA0B;oBAAC;iBAAO;gBAClCC,uBAAuB;oBAAC;oBAAsB;iBAAgB;gBAC9DC,kCAAkC;oBAAC;iBAAO;gBAC1CC,uCAAuC;oBAAC;iBAAO;gBAC/CT,kBAAkB;oBAAC;iBAAO;gBAC1BU,uCAAuC;YACzC,CAAA;IACF;AACF;AAEA,SAASC;IACP,OAAOnD,YAAY;QACjBgC,QAAQ;QACRC,MAAM;QACNC,aAAa;QACbzB,SAAS;YAAC2C,WAAW,KAAK;QAAI;QAC9BjB,QAAQ;YACNkB,MAAMzD;YACNwC,UAAU;gBAAC,KAAKvC;YAA4C;QAC9D;QACAyD,YAAY/C,gCAAgC;QAC5CgD,cAAc5C;QACd0B,SAAS,OAAOmB,SAASC;YACvB,MAAM1C,SAAS,MAAMT,oBAAoB;gBACvCc,YAAYoC,QAAQH,IAAI,CAAClC,WAAW;gBACpCG,cAAckC,QAAQH,IAAI,CAAChC,aAAa;gBACxCG,YAAYgC,QAAQH,IAAI,CAAC9B,WAAW;gBACpCG,eAAe8B,QAAQH,IAAI,CAAC5B,cAAc;gBAC1CG,yBAAyB4B,QAAQH,IAAI,CAAC1B,0BAA0B;gBAChEE,OAAO2B,QAAQH,IAAI,CAACxB,KAAK;YAC3B;YACA4B,MAAMC,IAAI,CAAC;YACX,OAAO5C,qBAAqBC;QAC9B;IACF;AACF;AAEA,OAAO,SAAS4C,0BAA0BlD,OAAiC;IACzE,MAAMsB,SAASvB,gBAAgBC;IAC/B,OAAO;QACLmD,QAAQ;QACRC,QAAQ;YACN/B,qCAAqCC;YACrCU,uCAAuCV;SACxC;IACH;AACF;AAEA,OAAO,SAAS+B;IACd,OAAO;QAACF,QAAQ;QAAIC,QAAQ;YAACV;SAAiC;IAAA;AAChE;AAEA,OAAO,SAASY,kBAAkBtD,OAAiC;IACjE,MAAMuD,iBAAiBL,0BAA0BlD;IACjD,MAAMwD,6BAA6BH;IACnC,OAAO;QACLF,QAAQ;QACRC,QAAQ;eAAIG,eAAeH,MAAM;eAAKI,2BAA2BJ,MAAM;SAAC;IAC1E;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/rate-limit.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAE1F,OAAO,EACL,KAAK,mBAAmB,EAIzB,MAAM,qBAAqB,CAAC;AAmC7B,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAgDD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mBAAmB,aAChD,cAAc,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAiB9F;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,mBAAmB,aAClD,cAAc,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAS3E;AAED;;;;;;GAMG;AACH,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,mBAAmB,aACrD,cAAc,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAmB3E"}
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/rate-limit.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAE1F,OAAO,EACL,KAAK,mBAAmB,EAIzB,MAAM,qBAAqB,CAAC;AAmD7B,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAgDD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mBAAmB,aAChD,cAAc,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAiB9F;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,mBAAmB,aAClD,cAAc,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAS3E;AAED;;;;;;GAMG;AACH,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,mBAAmB,aACrD,cAAc,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAmB3E"}