@shipfox/api-auth 17.0.0 → 19.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 (99) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +44 -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/entities/agent-access.d.ts +75 -0
  12. package/dist/core/entities/agent-access.d.ts.map +1 -0
  13. package/dist/core/entities/agent-access.js +3 -0
  14. package/dist/core/entities/agent-access.js.map +1 -0
  15. package/dist/core/errors.d.ts +3 -0
  16. package/dist/core/errors.d.ts.map +1 -1
  17. package/dist/core/errors.js +6 -0
  18. package/dist/core/errors.js.map +1 -1
  19. package/dist/core/runner-session-token.d.ts.map +1 -1
  20. package/dist/core/runner-session-token.js +2 -1
  21. package/dist/core/runner-session-token.js.map +1 -1
  22. package/dist/db/admin-user-moderation.d.ts.map +1 -1
  23. package/dist/db/admin-user-moderation.js +30 -26
  24. package/dist/db/admin-user-moderation.js.map +1 -1
  25. package/dist/db/admin-user-summary.d.ts +22 -0
  26. package/dist/db/admin-user-summary.d.ts.map +1 -1
  27. package/dist/db/admin-user-summary.js +75 -1
  28. package/dist/db/admin-user-summary.js.map +1 -1
  29. package/dist/db/admin-users.d.ts +11 -0
  30. package/dist/db/admin-users.d.ts.map +1 -1
  31. package/dist/db/admin-users.js +4 -1
  32. package/dist/db/admin-users.js.map +1 -1
  33. package/dist/db/agent-access.d.ts +159 -0
  34. package/dist/db/agent-access.d.ts.map +1 -0
  35. package/dist/db/agent-access.js +267 -0
  36. package/dist/db/agent-access.js.map +1 -0
  37. package/dist/db/db.d.ts +2604 -238
  38. package/dist/db/db.d.ts.map +1 -1
  39. package/dist/db/db.js +7 -0
  40. package/dist/db/db.js.map +1 -1
  41. package/dist/db/schema/agent-access.d.ts +1204 -0
  42. package/dist/db/schema/agent-access.d.ts.map +1 -0
  43. package/dist/db/schema/agent-access.js +267 -0
  44. package/dist/db/schema/agent-access.js.map +1 -0
  45. package/dist/db/schema/users.d.ts.map +1 -1
  46. package/dist/db/schema/users.js +8 -3
  47. package/dist/db/schema/users.js.map +1 -1
  48. package/dist/index.d.ts +3 -2
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +2 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/metrics/instance.d.ts +1 -1
  53. package/dist/metrics/instance.d.ts.map +1 -1
  54. package/dist/metrics/instance.js.map +1 -1
  55. package/dist/presentation/routes/administration.d.ts.map +1 -1
  56. package/dist/presentation/routes/administration.js +123 -20
  57. package/dist/presentation/routes/administration.js.map +1 -1
  58. package/dist/presentation/routes/email-verification/verify-email-resend.d.ts.map +1 -1
  59. package/dist/presentation/routes/email-verification/verify-email-resend.js +6 -1
  60. package/dist/presentation/routes/email-verification/verify-email-resend.js.map +1 -1
  61. package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
  62. package/dist/presentation/routes/rate-limit.js +10 -0
  63. package/dist/presentation/routes/rate-limit.js.map +1 -1
  64. package/dist/presentation/routes/registration/signup.d.ts.map +1 -1
  65. package/dist/presentation/routes/registration/signup.js +59 -50
  66. package/dist/presentation/routes/registration/signup.js.map +1 -1
  67. package/dist/tsconfig.test.tsbuildinfo +1 -1
  68. package/drizzle/0003_brainy_luckman.sql +6 -0
  69. package/drizzle/0004_yummy_runaways.sql +110 -0
  70. package/drizzle/meta/0003_snapshot.json +844 -0
  71. package/drizzle/meta/0004_snapshot.json +1751 -0
  72. package/drizzle/meta/_journal.json +14 -0
  73. package/package.json +7 -7
  74. package/src/core/admin-role.test.ts +70 -0
  75. package/src/core/administration.ts +78 -1
  76. package/src/core/auth.ts +3 -5
  77. package/src/core/entities/agent-access.ts +80 -0
  78. package/src/core/errors.ts +7 -0
  79. package/src/core/runner-session-token.test.ts +2 -0
  80. package/src/core/runner-session-token.ts +1 -0
  81. package/src/db/admin-user-moderation.ts +53 -45
  82. package/src/db/admin-user-summary.test.ts +228 -0
  83. package/src/db/admin-user-summary.ts +143 -1
  84. package/src/db/admin-users.ts +21 -1
  85. package/src/db/agent-access.test.ts +370 -0
  86. package/src/db/agent-access.ts +629 -0
  87. package/src/db/db.ts +14 -0
  88. package/src/db/schema/agent-access.ts +273 -0
  89. package/src/db/schema/users.ts +9 -3
  90. package/src/index.ts +6 -0
  91. package/src/metrics/instance.ts +1 -0
  92. package/src/presentation/routes/administration.test.ts +359 -2
  93. package/src/presentation/routes/administration.ts +142 -20
  94. package/src/presentation/routes/email-verification/verify-email-resend.ts +7 -1
  95. package/src/presentation/routes/rate-limit.ts +4 -0
  96. package/src/presentation/routes/registration/signup.ts +52 -41
  97. package/test/globalSetup.ts +1 -1
  98. package/test/routes.ts +3 -1
  99. 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":"AAAA,SAAQA,UAAU,QAAO,cAAc;AACvC,SACEC,SAAS,EACTC,6BAA6B,EAC7BC,0BAA0B,QACrB,4BAA4B;AACnC,SACEC,yBAAyB,EACzBC,kCAAkC,EAClCC,uCAAuC,EACvCC,8BAA8B,EAC9BC,6BAA6B,EAC7BC,iCAAiC,EACjCC,wBAAwB,EACxBC,4BAA4B,EAC5BC,yBAAyB,EACzBC,yBAAyB,EACzBC,0BAA0B,EAC1BC,6BAA6B,EAC7BC,qCAAqC,EACrCC,0BAA0B,EAC1BC,8BAA8B,EAC9BC,yCAAyC,EACzCC,6CAA6C,EAC7CC,kCAAkC,QAC7B,wBAAwB;AAE/B,SAAQC,uBAAuB,EAAEC,uBAAuB,QAAO,wBAAwB;AACvF,SAAQC,WAAW,EAAEC,WAAW,QAAwB,wBAAwB;AAEhF,SAAQC,CAAC,QAAO,MAAM;AACtB,SAAQC,gBAAgB,QAAO,sBAAsB;AACrD,SACEC,wBAAwB,EACxBC,4BAA4B,EAC5BC,sBAAsB,EACtBC,sBAAsB,EACtBC,eAAe,EACfC,+BAA+B,EAC/BC,2BAA2B,EAC3BC,wBAAwB,EACxBC,+BAA+B,EAC/BC,wBAAwB,QACnB,0BAA0B;AAMjC,SACEC,yBAAyB,EACzBC,4BAA4B,EAC5BC,uBAAuB,EACvBC,6BAA6B,EAC7BC,sBAAsB,EACtBC,mCAAmC,EACnCC,0BAA0B,EAC1BC,qBAAqB,EACrBC,0BAA0B,EAC1BC,yBAAyB,EACzBC,iCAAiC,EACjCC,+BAA+B,EAC/BC,uBAAuB,EACvBC,mBAAmB,EACnBC,iBAAiB,QACZ,kBAAkB;AACzB,SAAQC,gBAAgB,QAAO,iCAAiC;AAChE,SAAQC,SAAS,QAAO,4BAA4B;AACpD,SAAQC,kCAAkC,EAAEC,+BAA+B,QAAO,kBAAkB;AAEpG,MAAMC,0BAA0B;AAEhC,SAASC,eAAeC,OAAuB;IAC7C,MAAMC,SAASP,iBAAiBM;IAChC,IAAI,CAACC,QAAQ;QACX,MAAM,IAAIpC,YAAY,2BAA2B,gBAAgB;YAACqC,QAAQ;QAAG;IAC/E;IACA,OAAOD,OAAOE,MAAM;AACtB;AAEA,SAASC,sBAAsBJ,OAAuB;IACpD,MAAMK,QAAQL,QAAQM,OAAO,CAAC,kBAAkB;IAChD,MAAMC,MAAMC,MAAMC,OAAO,CAACJ,SAASA,KAAK,CAAC,EAAE,GAAGA;IAC9C,IAAI,CAACE,OAAOA,IAAIG,IAAI,GAAGC,MAAM,KAAK,KAAKJ,IAAII,MAAM,GAAGb,yBAAyB;QAC3E,MAAM,IAAIjC,YAAY,sCAAsC,4BAA4B;YACtFqC,QAAQ;QACV;IACF;IACA,OAAOK;AACT;AAEA,eAAeK,6BAA6BZ,OAAuB;IACjE,MAAMhC,iBAAiB;QAACmC,QAAQJ,eAAeC;QAAUa,aAAa;IAAgB;AACxF;AAEA,SAASC,gBAAgBC,KAAiB;IACxC,OAAO;QACLC,IAAID,MAAMC,EAAE;QACZC,SAASF,MAAMZ,MAAM;QACrBe,MAAMH,MAAMG,IAAI;QAChBC,YAAYJ,MAAMK,SAAS,EAAEC,iBAAiB;QAC9CC,YAAYP,MAAMQ,SAAS,CAACF,WAAW;QACvCG,YAAYT,MAAMU,SAAS,CAACJ,WAAW;IACzC;AACF;AAEA,SAASK,8BAA8BC,IAA8B;IACnE,OAAO;QACLX,IAAIW,KAAKX,EAAE;QACXY,OAAOD,KAAKC,KAAK;QACjBC,MAAMF,KAAKE,IAAI;QACf3B,QAAQyB,KAAKzB,MAAM;QACnB4B,mBAAmBH,KAAKI,eAAe,EAAEV,iBAAiB;QAC1DC,YAAYK,KAAKJ,SAAS,CAACF,WAAW;QACtCW,YAAYL,KAAKM,SAAS;IAC5B;AACF;AAEA,SAASC,+BAA+BnB,KAAgC;IACtE,OAAO;QACLoB,UAAUpB,MAAMqB,OAAO;QACvBlB,MAAMH,MAAMG,IAAI;QAChBI,YAAYP,MAAMQ,SAAS,CAACF,WAAW;QACvCF,YAAYJ,MAAMK,SAAS,EAAEC,iBAAiB;QAC9CM,MAAMZ,MAAMY,IAAI;IAClB;AACF;AAEA,SAASU,+BAA+BC,MAGvC;IACC,OAAO;QACL,GAAGZ,8BAA8BY,OAAOX,IAAI,CAAC;QAC7CY,gBAAgBD,OAAOE,aAAa;IACtC;AACF;AAEA,SAASC,6BAA6BC,KAAc;IAClD,IAAIA,iBAAiB3D,wBAAwB;QAC3C,MAAM,IAAIlB,YAAY,+BAA+B,aAAa;YAChEqC,QAAQ;YACRyC,SAAS;gBAACC,eAAeF,MAAM7B,WAAW;YAAA;QAC5C;IACF;IACA,IAAI6B,iBAAiBpD,iCAAiC;QACpD,MAAM,IAAIzB,YAAY,8BAA8B,2BAA2B;YAC7EqC,QAAQ;QACV;IACF;IACA,IAAIwC,iBAAiB/D,2BAA2B;QAC9C,MAAM,IAAId,YAAY,4CAA4C,oBAAoB;YACpFqC,QAAQ;QACV;IACF;IACA,IAAIwC,iBAAiB9D,8BAA8B;QACjD,MAAM,IAAIf,YAAY,sCAAsC,wBAAwB;YAClFqC,QAAQ;QACV;IACF;IACA,IAAIwC,iBAAiB7D,yBAAyB;QAC5C,MAAM,IAAIhB,YAAY,iCAAiC,aAAa;YAACqC,QAAQ;QAAG;IAClF;IACA,IAAIwC,iBAAiBjD,mBAAmB;QACtC,MAAM,IAAI5B,YAAY,kBAAkB,aAAa;YAACqC,QAAQ;QAAG;IACnE;IACA,IAAIwC,iBAAiBlD,qBAAqB;QACxC,MAAM,IAAI3B,YAAY,sDAAsD,cAAc;YACxFqC,QAAQ;QACV;IACF;IACA,IAAIwC,iBAAiB5D,+BAA+B;QAClD,MAAM,IAAIjB,YACR,4DACA,0BACA;YAACqC,QAAQ;QAAG;IAEhB;IACA,IAAIwC,iBAAiBvD,4BAA4B;QAC/C,MAAM,IAAItB,YAAY,6BAA6B,0BAA0B;YAACqC,QAAQ;QAAG;IAC3F;IACA,IAAIwC,iBAAiBzD,4BAA4B;QAC/C,MAAM,IAAIpB,YAAY,+BAA+B,2BAA2B;YAC9EqC,QAAQ;QACV;IACF;IACA,IAAIwC,iBAAiB1D,qCAAqC;QACxD,MAAM,IAAInB,YACR,uCACA,oCACA;YAACqC,QAAQ;QAAG;IAEhB;IACA,IAAIwC,iBAAiBrD,mCAAmC;QACtD,MAAM,IAAIxB,YAAY,+BAA+B,mCAAmC;YACtFqC,QAAQ;QACV;IACF;IACA,2EAA2E;IAC3E,0EAA0E;IAC1E,2EAA2E;IAC3E,gEAAgE;IAChE,IAAIwC,iBAAiBxD,yBAAyBwD,iBAAiBnD,yBAAyB;QACtF,MAAM,IAAI1B,YAAY,+BAA+B,mCAAmC;YACtFqC,QAAQ;QACV;IACF;IACA,IAAIwC,iBAAiBtD,2BAA2B;QAC9C,MAAM,IAAIvB,YAAY,qCAAqC,yBAAyB;YAClFqC,QAAQ;QACV;IACF;IACA,MAAMwC;AACR;AAEA,MAAMG,iBAAiB/E,YAAY;IACjCgF,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNC,MAAMrG;QACNsG,UAAU;YAAC,KAAKrG;QAAiC;IACnD;IACAsG,YAAYvD,gCAAgC;IAC5CwD,cAAcZ;IACda,SAAS,OAAOtD,SAASuD;QACvB,MAAMC,UAAUzD,eAAeC;QAC/B,MAAMe,QAAQ,MAAM9C,yBAAyB;YAC3CuF;YACAC,gBAAgBzD,QAAQkD,IAAI,CAACQ,eAAe;YAC5CC,gBAAgBvD,sBAAsBJ;YACtCwC,eAAexC,QAAQgB,EAAE;QAC3B;QACAuC,MAAMK,IAAI,CAAC;QACX,OAAO9C,gBAAgBC;IACzB;AACF;AAEA,MAAM8C,sBAAsB/F,YAAY;IACtCgF,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QAACE,UAAU;YAAC,KAAK1G;QAAyB;IAAC;IACnD2G,YAAYvD,gCAAgC;IAC5CwD,cAAcZ;IACda,SAAS,UAAa,CAAA;YAACQ,OAAO,MAAM3F;QAAwB,CAAA;AAC9D;AAEA,MAAM4F,YAAYjG,YAAY;IAC5BgF,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNe,aAAa7G;QACbgG,UAAU;YAAC,KAAK/F;QAA6B;IAC/C;IACAiG,cAAcZ;IACda,SAAS,OAAOtD;QACd,MAAM,EAACiE,KAAK,EAAEC,MAAM,EAAC,GAAGlE,QAAQmE,KAAK;QACrC,MAAMC,gBAAgBzG,wBAAwBuG;QAC9C,IAAIA,UAAU,CAACE,eAAe;YAC5B,MAAM,IAAIvG,YAAY,kBAAkB,kBAAkB;gBAACqC,QAAQ;YAAG;QACxE;QAEA,MAAMoC,SAAS,MAAMhE,gCAAgC;YACnDkF,SAASzD,eAAeC;YACxBiE;YACA,GAAIG,gBAAgB;gBAACF,QAAQE;YAAa,IAAI,CAAC,CAAC;QAClD;QACA,OAAO;YACLC,QAAQ/B,OAAO+B,MAAM,CAACC,GAAG,CAACpC;YAC1BqC,aAAajC,OAAOkC,UAAU,GAAG5G,wBAAwB0E,OAAOkC,UAAU,IAAI;QAChF;IACF;AACF;AAEA,MAAMC,kBAAkB3G,YAAY;IAClCgF,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNe,aAAatH;QACbyG,UAAU;YAAC,KAAKvG;QAA8B;IAChD;IACAwG,YAAY;QAACxC;QAA8Bf,gCAAgC;KAAU;IACrFwD,cAAcZ;IACda,SAAS,OAAOtD;QACd,MAAM,EAACgB,EAAE,EAAEC,SAASd,MAAM,EAAEyB,KAAK,EAAC,GAAG5B,QAAQmE,KAAK;QAClD,MAAMO,WAAW1D,MAAMb;QACvB,MAAMqD,UAAUzD,eAAeC;QAC/B,MAAM2B,OAAO+C,WACT,MAAMxG,6BAA6B;YAACsF;YAASxC,IAAI0D;QAAQ,KACzD9C,QACE,MAAM1D,6BAA6B;YAACsF;YAAS5B;QAAK,KAClD+C;QACN,IAAI,CAAChD,MAAM,MAAM,IAAIlC,kBAAkBiF,YAAY9C,SAAS;QAC5D,OAAOF,8BAA8BC;IACvC;AACF;AAEA,MAAMiD,mBAAmB9G,YAAY;IACnCgF,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACN4B,QAAQ9G,EAAE+G,MAAM,CAAC;YAAC3E,QAAQpC,EAAEgH,MAAM,GAAGC,IAAI;QAAE;QAC3C9B,MAAMxF;QACNyF,UAAU;YAAC,KAAKxG;QAAuC;IACzD;IACA0G,cAAcZ;IACda,SAAS,OAAOtD;QACd,MAAMsC,SAAS,MAAM5D,yBAAyB;YAC5C8E,SAASzD,eAAeC;YACxBG,QAAQH,QAAQ6E,MAAM,CAAC1E,MAAM;YAC7B8E,QAAQjF,QAAQkD,IAAI,CAAC+B,MAAM;YAC3BtB,gBAAgBvD,sBAAsBJ;YACtCwC,eAAexC,QAAQgB,EAAE;QAC3B;QACA,OAAOqB,+BAA+BC;IACxC;AACF;AAEA,MAAM4C,sBAAsBpH,YAAY;IACtCgF,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACN4B,QAAQ9G,EAAE+G,MAAM,CAAC;YAAC3E,QAAQpC,EAAEgH,MAAM,GAAGC,IAAI;QAAE;QAC3C9B,MAAM7F;QACN8F,UAAU;YAAC,KAAKxG;QAAuC;IACzD;IACA0G,cAAcZ;IACda,SAAS,OAAOtD;QACd,MAAMsC,SAAS,MAAM/D,4BAA4B;YAC/CiF,SAASzD,eAAeC;YACxBG,QAAQH,QAAQ6E,MAAM,CAAC1E,MAAM;YAC7B,GAAIH,QAAQkD,IAAI,CAAC+B,MAAM,GAAG;gBAACA,QAAQjF,QAAQkD,IAAI,CAAC+B,MAAM;YAAA,IAAI,CAAC,CAAC;YAC5DtB,gBAAgBvD,sBAAsBJ;YACtCwC,eAAexC,QAAQgB,EAAE;QAC3B;QACA,OAAOqB,+BAA+BC;IACxC;AACF;AAEA,MAAM6C,0BAA0BrH,YAAY;IAC1CgF,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACN4B,QAAQ9G,EAAE+G,MAAM,CAAC;YAAC3E,QAAQpC,EAAEgH,MAAM,GAAGC,IAAI;QAAE;QAC3C9B,MAAM1F;QACN2F,UAAU;YAAC,KAAK1F;QAA6C;IAC/D;IACA4F,cAAcZ;IACda,SAAS,OAAOtD;QACd,MAAMsC,SAAS,MAAM7D,gCAAgC;YACnD+E,SAASzD,eAAeC;YACxBG,QAAQH,QAAQ6E,MAAM,CAAC1E,MAAM;YAC7B,GAAIH,QAAQkD,IAAI,CAAC+B,MAAM,GAAG;gBAACA,QAAQjF,QAAQkD,IAAI,CAAC+B,MAAM;YAAA,IAAI,CAAC,CAAC;YAC5DtB,gBAAgBvD,sBAAsBJ;YACtCwC,eAAexC,QAAQgB,EAAE;QAC3B;QACA,OAAO;YACL,GAAGqB,+BAA+BC,OAAO;YACzC8C,kBAAkB9C,OAAO+C,eAAe;QAC1C;IACF;AACF;AAEA,MAAMC,aAAaxH,YAAY;IAC7BgF,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNC,MAAMnG;QACNoG,UAAU;YAAC,KAAKnG;QAA4B;IAC9C;IACAqG,cAAcZ;IACda,SAAS,OAAOtD,SAASuD;QACvB,MAAMC,UAAUzD,eAAeC;QAC/B,MAAMe,QAAQ,MAAM3C,uBAAuB;YACzCoF;YACArD,QAAQH,QAAQkD,IAAI,CAACjC,OAAO;YAC5BC,MAAMlB,QAAQkD,IAAI,CAAChC,IAAI;YACvB+D,QAAQjF,QAAQkD,IAAI,CAAC+B,MAAM;YAC3BtB,gBAAgBvD,sBAAsBJ;YACtCwC,eAAexC,QAAQgB,EAAE;QAC3B;QACAuC,MAAMK,IAAI,CAAC;QACX,OAAO9C,gBAAgBC;IACzB;AACF;AAEA,MAAMwE,cAAczH,YAAY;IAC9BgF,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACN4B,QAAQ9G,EAAE+G,MAAM,CAAC;YAAC1C,SAASrE,EAAEgH,MAAM,GAAGC,IAAI;QAAE;QAC5C9B,MAAM5F;QACN6F,UAAU;YAAC,KAAK5F;QAA8B;IAChD;IACA8F,cAAcZ;IACda,SAAS,OAAOtD;QACd,MAAMe,QAAQ,MAAMvC,yBAAyB;YAC3CgF,SAASzD,eAAeC;YACxBoC,SAASpC,QAAQ6E,MAAM,CAACzC,OAAO;YAC/B6C,QAAQjF,QAAQkD,IAAI,CAAC+B,MAAM;YAC3BtB,gBAAgBvD,sBAAsBJ;YACtCwC,eAAexC,QAAQgB,EAAE;QAC3B;QACA,OAAOF,gBAAgBC;IACzB;AACF;AAEA,OAAO,MAAMyE,uBAAmCjJ,8BAA8B;IAC5EkJ,QAAQ;IACRC,MAAMpJ;IACNqJ,QAAQ;QAAC9C;QAAgBkB;QAAWuB;QAAYC;KAAY;AAC9D,GAAG;AAEH,OAAO,MAAMK,gCAA4CrJ,8BAA8B;IACrFkJ,QAAQ;IACRC,MAAMpJ;IACNqJ,QAAQ;QAAC9B;KAAoB;AAC/B,GAAG;AAEH,SAASgC,2BAA2BC,UAAuC;IACzE,OAAOhI,YAAY;QACjBgF,QAAQ;QACRC,MAAM;QACNC,aACE;QACFC,QAAQ;YACN4B,QAAQ9G,EAAE+G,MAAM,CAAC;gBAAC3E,QAAQpC,EAAEgH,MAAM,GAAGC,IAAI;YAAE;YAC3C9B,MAAMhG;YACNiG,UAAU;gBAAC,KAAKlG;YAAyB;QAC3C;QACA,0EAA0E;QAC1E,iEAAiE;QACjE,sEAAsE;QACtE,2EAA2E;QAC3E,oEAAoE;QACpEmG,YAAY;YACVxD,mCAAmC;YACnC,CAACI;gBACCxD,2BAA2BwD;gBAC3B,OAAO2E;YACT;SACD;QACDtB,cAAcZ;QACda,SAAS,OAAOtD;YACd,MAAMC,SAASP,iBAAiBM;YAChC,MAAMsC,SAAS,MAAMjE,gBAAgB;gBACnCmF,SAASzD,eAAeC;gBACxB,GAAIC,QAAQ8F,iBAAiB;oBAACC,qBAAqB/F,OAAO8F,cAAc;gBAAA,IAAI,CAAC,CAAC;gBAC9EE,cAAcjG,QAAQ6E,MAAM,CAAC1E,MAAM;gBACnC8E,QAAQjF,QAAQkD,IAAI,CAAC+B,MAAM;gBAC3BtB,gBAAgBvD,sBAAsBJ;gBACtC,sEAAsE;gBACtE,qEAAqE;gBACrE,qEAAqE;gBACrE,sEAAsE;gBACtE,kBAAkB;gBAClBwC,eAAenG;gBACfyJ;YACF;YACA,mEAAmE;YACnE,kEAAkE;YAClE,OAAO;gBACLI,OAAO5D,OAAO4D,KAAK;gBACnBC,YAAY7D,OAAO8D,SAAS,CAAC/E,WAAW;gBACxCgF,aAAa,IAAIC,OAAOjF,WAAW;gBACnCkF,iBAAiBjE,OAAOyD,cAAc;gBACtCpE,MAAMhC,UAAU2C,OAAOX,IAAI;YAC7B;QACF;IACF;AACF;AAEA,OAAO,SAAS6E,+BACdV,UAAuC;IAEvC,OAAO;QACLvJ,8BAA8B;YAC5BkJ,QAAQ;YACRC,MAAMpJ;YACNqJ,QAAQ;gBAAClB;gBAAiBG;gBAAkBM;gBAAqBC;aAAwB;QAC3F;QACA,yEAAyE;QACzE,kEAAkE;QAClE,sEAAsE;QACtE,yEAAyE;QACzE;YACEM,QAAQ;YACRC,MAAMpJ;YACNqJ,QAAQ;gBAACE,2BAA2BC;aAAY;QAClD;KACD;AACH"}
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":"AAAA,SAAQA,UAAU,QAAO,cAAc;AACvC,SACEC,SAAS,EACTC,6BAA6B,EAC7BC,0BAA0B,QACrB,4BAA4B;AACnC,SACEC,yBAAyB,EACzBC,qCAAqC,EACrCC,wCAAwC,EACxCC,kCAAkC,EAClCC,uCAAuC,EACvCC,8BAA8B,EAC9BC,6BAA6B,EAC7BC,iCAAiC,EACjCC,wBAAwB,EACxBC,4BAA4B,EAC5BC,yBAAyB,EACzBC,yBAAyB,EACzBC,0BAA0B,EAC1BC,6BAA6B,EAC7BC,qCAAqC,EACrCC,0BAA0B,EAC1BC,8BAA8B,EAC9BC,yCAAyC,EACzCC,6CAA6C,EAC7CC,kCAAkC,QAC7B,wBAAwB;AAE/B,SAAQC,uBAAuB,EAAEC,uBAAuB,QAAO,wBAAwB;AACvF,SAAQC,WAAW,EAAEC,WAAW,QAAwB,wBAAwB;AAEhF,SAAQC,CAAC,QAAO,MAAM;AACtB,SAAQC,gBAAgB,QAAO,sBAAsB;AACrD,SACEC,wBAAwB,EACxBC,4BAA4B,EAC5BC,sBAAsB,EACtBC,sBAAsB,EACtBC,eAAe,EACfC,+BAA+B,EAC/BC,sBAAsB,EACtBC,2BAA2B,EAC3BC,wBAAwB,EACxBC,+BAA+B,EAC/BC,wBAAwB,QACnB,0BAA0B;AAMjC,SACEC,yBAAyB,EACzBC,4BAA4B,EAC5BC,uBAAuB,EACvBC,6BAA6B,EAC7BC,sBAAsB,EACtBC,mCAAmC,EACnCC,0BAA0B,EAC1BC,qBAAqB,EACrBC,0BAA0B,EAC1BC,yBAAyB,EACzBC,iCAAiC,EACjCC,+BAA+B,EAC/BC,4CAA4C,EAC5CC,uBAAuB,EACvBC,mBAAmB,EACnBC,iBAAiB,QACZ,kBAAkB;AACzB,SAAQC,gBAAgB,QAAO,iCAAiC;AAChE,SAAQC,SAAS,QAAO,4BAA4B;AACpD,SAAQC,kCAAkC,EAAEC,+BAA+B,QAAO,kBAAkB;AAEpG,MAAMC,0BAA0B;AAEhC,SAASC,eAAeC,OAAuB;IAC7C,MAAMC,SAASP,iBAAiBM;IAChC,IAAI,CAACC,QAAQ;QACX,MAAM,IAAItC,YAAY,2BAA2B,gBAAgB;YAACuC,QAAQ;QAAG;IAC/E;IACA,OAAOD,OAAOE,MAAM;AACtB;AAEA,SAASC,sBAAsBJ,OAAuB;IACpD,MAAMK,QAAQL,QAAQM,OAAO,CAAC,kBAAkB;IAChD,MAAMC,MAAMC,MAAMC,OAAO,CAACJ,SAASA,KAAK,CAAC,EAAE,GAAGA;IAC9C,IAAI,CAACE,OAAOA,IAAIG,IAAI,GAAGC,MAAM,KAAK,KAAKJ,IAAII,MAAM,GAAGb,yBAAyB;QAC3E,MAAM,IAAInC,YAAY,sCAAsC,4BAA4B;YACtFuC,QAAQ;QACV;IACF;IACA,OAAOK;AACT;AAEA,eAAeK,6BAA6BZ,OAAuB;IACjE,MAAMlC,iBAAiB;QAACqC,QAAQJ,eAAeC;QAAUa,aAAa;IAAgB;AACxF;AAEA,SAASC,gBAAgBC,KAAiB;IACxC,OAAO;QACLC,IAAID,MAAMC,EAAE;QACZC,SAASF,MAAMZ,MAAM;QACrBe,MAAMH,MAAMG,IAAI;QAChBC,YAAYJ,MAAMK,SAAS,EAAEC,iBAAiB;QAC9CC,YAAYP,MAAMQ,SAAS,CAACF,WAAW;QACvCG,YAAYT,MAAMU,SAAS,CAACJ,WAAW;IACzC;AACF;AAEA,SAASK,8BAA8BC,IAA8B;IACnE,OAAO;QACLX,IAAIW,KAAKX,EAAE;QACXY,OAAOD,KAAKC,KAAK;QACjBC,MAAMF,KAAKE,IAAI;QACf3B,QAAQyB,KAAKzB,MAAM;QACnB4B,mBAAmBH,KAAKI,eAAe,EAAEV,iBAAiB;QAC1DC,YAAYK,KAAKJ,SAAS,CAACF,WAAW;QACtCW,YAAYL,KAAKM,SAAS;IAC5B;AACF;AAEA,SAASC,+BAA+BnB,KAAgC;IACtE,OAAO;QACLoB,UAAUpB,MAAMqB,OAAO;QACvBlB,MAAMH,MAAMG,IAAI;QAChBI,YAAYP,MAAMQ,SAAS,CAACF,WAAW;QACvCF,YAAYJ,MAAMK,SAAS,EAAEC,iBAAiB;QAC9CM,MAAMZ,MAAMY,IAAI;IAClB;AACF;AAEA,SAASU,+BAA+BC,MAGvC;IACC,OAAO;QACL,GAAGZ,8BAA8BY,OAAOX,IAAI,CAAC;QAC7CY,gBAAgBD,OAAOE,aAAa;IACtC;AACF;AAEA,SAASC,2BAA2BC,KAAa;IAC/C,IAAIA,UAAU,GAAG,OAAO;IACxB,IAAIA,SAAS,IAAI,OAAO;IACxB,IAAIA,SAAS,IAAI,OAAO;IACxB,OAAO;AACT;AAEA,SAASC,kCAAkCC,MAY1C;IACC,IAAI;QACFA,OAAO5C,OAAO,CAAC6C,GAAG,CAACC,IAAI,CACrB;YACEC,SAASH,OAAOG,OAAO;YACvBC,cAAc;YACdC,YAAY;YACZC,WAAWN,OAAO5C,OAAO,CAACgB,EAAE;YAC5BsB,QAAQM,OAAOO,OAAO;YACtBA,SAASP,OAAOO,OAAO;YACvBC,YAAYR,OAAOQ,UAAU;YAC7BC,mBAAmBZ,2BAA2BG,OAAOU,WAAW;YAChEC,gBAAgBX,OAAOW,cAAc;YACrCC,iBAAiBZ,OAAOY,eAAe;QACzC,GACA;IAEJ,EAAE,OAAM;IACN,sDAAsD;IACxD;AACF;AAEA,SAASC,uCAAuCC,KAAc;IAC5D,4EAA4E;IAC5E,4EAA4E;IAC5E,4EAA4E;IAC5E,yCAAyC;IACzC,IAAIA,iBAAiBzE,yBAAyByE,iBAAiBnE,yBAAyB;QACtF,OAAO,IAAI5B,YAAY,+BAA+B,mCAAmC;YACvFuC,QAAQ;QACV;IACF;IACA,IAAIwD,iBAAiBvE,2BAA2B;QAC9C,OAAO,IAAIxB,YAAY,qCAAqC,yBAAyB;YACnFuC,QAAQ;QACV;IACF;IACA,OAAOyD;AACT;AAEA,SAASC,6BAA6BF,KAAc;IAClD,MAAMG,gCAAgCJ,uCAAuCC;IAC7E,IAAIG,+BAA+B,MAAMA;IACzC,IAAIH,iBAAiB5E,wBAAwB;QAC3C,MAAM,IAAInB,YAAY,+BAA+B,aAAa;YAChEuC,QAAQ;YACR4D,SAAS;gBAACC,eAAeL,MAAM7C,WAAW;YAAA;QAC5C;IACF;IACA,IAAI6C,iBAAiBpE,8CAA8C;QACjE,MAAM,IAAI3B,YAAY+F,MAAMM,OAAO,EAAE,oBAAoB;YAAC9D,QAAQ;QAAG;IACvE;IACA,IAAIwD,iBAAiBrE,iCAAiC;QACpD,MAAM,IAAI1B,YAAY,8BAA8B,2BAA2B;YAC7EuC,QAAQ;QACV;IACF;IACA,IAAIwD,iBAAiBhF,2BAA2B;QAC9C,MAAM,IAAIf,YAAY,4CAA4C,oBAAoB;YACpFuC,QAAQ;QACV;IACF;IACA,IAAIwD,iBAAiB/E,8BAA8B;QACjD,MAAM,IAAIhB,YAAY,sCAAsC,wBAAwB;YAClFuC,QAAQ;QACV;IACF;IACA,IAAIwD,iBAAiB9E,yBAAyB;QAC5C,MAAM,IAAIjB,YAAY,iCAAiC,aAAa;YAACuC,QAAQ;QAAG;IAClF;IACA,IAAIwD,iBAAiBjE,mBAAmB;QACtC,MAAM,IAAI9B,YAAY,kBAAkB,aAAa;YAACuC,QAAQ;QAAG;IACnE;IACA,IAAIwD,iBAAiBlE,qBAAqB;QACxC,MAAM,IAAI7B,YAAY,sDAAsD,cAAc;YACxFuC,QAAQ;QACV;IACF;IACA,IAAIwD,iBAAiB7E,+BAA+B;QAClD,MAAM,IAAIlB,YACR,4DACA,0BACA;YAACuC,QAAQ;QAAG;IAEhB;IACA,IAAIwD,iBAAiBxE,4BAA4B;QAC/C,MAAM,IAAIvB,YAAY,6BAA6B,0BAA0B;YAACuC,QAAQ;QAAG;IAC3F;IACA,IAAIwD,iBAAiB1E,4BAA4B;QAC/C,MAAM,IAAIrB,YAAY,+BAA+B,2BAA2B;YAC9EuC,QAAQ;QACV;IACF;IACA,IAAIwD,iBAAiB3E,qCAAqC;QACxD,MAAM,IAAIpB,YACR,uCACA,oCACA;YAACuC,QAAQ;QAAG;IAEhB;IACA,IAAIwD,iBAAiBtE,mCAAmC;QACtD,MAAM,IAAIzB,YAAY,+BAA+B,mCAAmC;YACtFuC,QAAQ;QACV;IACF;IACA,MAAMwD;AACR;AAEA,MAAMO,iBAAiBrG,YAAY;IACjCsG,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNC,MAAM3H;QACN4H,UAAU;YAAC,KAAK3H;QAAiC;IACnD;IACA4H,YAAY3E,gCAAgC;IAC5C4E,cAAcb;IACdc,SAAS,OAAO1E,SAAS2E;QACvB,MAAM5B,UAAUhD,eAAeC;QAC/B,MAAMe,QAAQ,MAAMhD,yBAAyB;YAC3CgF;YACA6B,gBAAgB5E,QAAQsE,IAAI,CAACO,eAAe;YAC5CC,gBAAgB1E,sBAAsBJ;YACtCwC,eAAexC,QAAQgB,EAAE;QAC3B;QACA2D,MAAMI,IAAI,CAAC;QACX,OAAOjE,gBAAgBC;IACzB;AACF;AAEA,MAAMiE,sBAAsBpH,YAAY;IACtCsG,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QAACE,UAAU;YAAC,KAAKlI;QAAyB;IAAC;IACnDmI,YAAY3E,gCAAgC;IAC5C4E,cAAcb;IACdc,SAAS,UAAa,CAAA;YAACO,OAAO,MAAMhH;QAAwB,CAAA;AAC9D;AAEA,MAAMiH,YAAYtH,YAAY;IAC5BsG,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNc,aAAalI;QACbsH,UAAU;YAAC,KAAKrH;QAA6B;IAC/C;IACAuH,cAAcb;IACdc,SAAS,OAAO1E;QACd,MAAM,EAACoF,KAAK,EAAEC,MAAM,EAAC,GAAGrF,QAAQsF,KAAK;QACrC,MAAMC,gBAAgB9H,wBAAwB4H;QAC9C,IAAIA,UAAU,CAACE,eAAe;YAC5B,MAAM,IAAI5H,YAAY,kBAAkB,kBAAkB;gBAACuC,QAAQ;YAAG;QACxE;QAEA,MAAMoC,SAAS,MAAMlE,gCAAgC;YACnD2E,SAAShD,eAAeC;YACxBoF;YACA,GAAIG,gBAAgB;gBAACF,QAAQE;YAAa,IAAI,CAAC,CAAC;QAClD;QACA,OAAO;YACLC,QAAQlD,OAAOkD,MAAM,CAACC,GAAG,CAACvD;YAC1BwD,aAAapD,OAAOqD,UAAU,GAAGjI,wBAAwB4E,OAAOqD,UAAU,IAAI;QAChF;IACF;AACF;AAEA,MAAMC,kBAAkBhI,YAAY;IAClCsG,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNc,aAAa3I;QACb+H,UAAU;YAAC,KAAK7H;QAA8B;IAChD;IACA8H,YAAY;QAAC5D;QAA8Bf,gCAAgC;KAAU;IACrF4E,cAAcb;IACdc,SAAS,OAAO1E;QACd,MAAM,EAACgB,EAAE,EAAEC,SAASd,MAAM,EAAEyB,KAAK,EAAC,GAAG5B,QAAQsF,KAAK;QAClD,MAAMO,WAAW7E,MAAMb;QACvB,MAAM4C,UAAUhD,eAAeC;QAC/B,IAAI2B;QACJ,IAAIkE,UAAUlE,OAAO,MAAM3D,6BAA6B;YAAC+E;YAAS/B,IAAI6E;QAAQ;aACzE,IAAIjE,OAAOD,OAAO,MAAM3D,6BAA6B;YAAC+E;YAASnB;QAAK;QACzE,IAAI,CAACD,MAAM,MAAM,IAAIlC,kBAAkBoG,YAAYjE,SAAS;QAC5D,OAAOF,8BAA8BC;IACvC;AACF;AAEA,MAAMmE,qBAAqBlI,YAAY;IACrCsG,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNc,aAAa7I;QACbiI,UAAU;YAAC,KAAKhI;QAAwC;IAC1D;IACAiI,YAAY5E,mCAAmC;IAC/C6E,cAAcb;IACdc,SAAS,OAAO1E;QACd,MAAM+C,UAAUhD,eAAeC;QAC/B,MAAM+F,YAAYC,YAAYC,GAAG;QACjC,MAAM,EACJC,MAAM,EACNhG,MAAM,EACNiG,wBAAwBC,qBAAqB,EAC7ChB,KAAK,EACLC,MAAM,EACP,GAAGrF,QAAQsF,KAAK;QACjB,IAAIhC,cAAc;QAClB,IAAIE,kBAAkB;QACtB,IAAIL,UAAkC;QAEtC,IAAI;YACF,MAAMoC,gBAAgB9H,wBAAwB4H;YAC9C,IAAIA,UAAU,CAACE,eAAe;gBAC5B,MAAM,IAAI5H,YAAY,kBAAkB,kBAAkB;oBAACuC,QAAQ;gBAAG;YACxE;YAEA,MAAMoC,SAAS,MAAMjE,uBAAuB;gBAC1C0E;gBACAqC;gBACA,GAAIG,gBAAgB;oBAACF,QAAQE;gBAAa,IAAI,CAAC,CAAC;gBAChD,GAAIW,WAAWvC,YAAY;oBAACuC;gBAAM,IAAI,CAAC,CAAC;gBACxC,GAAIhG,WAAWyD,YAAY;oBAACzD;gBAAM,IAAI,CAAC,CAAC;gBACxC,GAAIkG,0BAA0BzC,YAAY;oBAAC0C,UAAUD;gBAAqB,IAAI,CAAC,CAAC;YAClF;YACA9C,cAAchB,OAAOgE,KAAK,CAAC3F,MAAM;YACjC6C,kBAAkBlB,OAAOqD,UAAU,KAAK;YACxCxC,UAAU;YAEV,OAAO;gBACLmD,OAAOhE,OAAOgE,KAAK,CAACb,GAAG,CAAC/D;gBACxBgE,aAAapD,OAAOqD,UAAU,GAAGjI,wBAAwB4E,OAAOqD,UAAU,IAAI;YAChF;QACF,SAAU;YACRhD,kCAAkC;gBAChC3C;gBACA+C;gBACAI;gBACAC,YAAYmD,KAAKC,KAAK,CAACR,YAAYC,GAAG,KAAKF;gBAC3CzC;gBACAC,gBAAgB;oBACd2C,QAAQA,WAAWvC;oBACnBzD,QAAQA,WAAWyD;oBACnByC,uBAAuBA,0BAA0BzC;gBACnD;gBACAH;YACF;QACF;IACF;AACF;AAEA,MAAMiD,mBAAmB7I,YAAY;IACnCsG,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNzB,QAAQ/E,EAAE6I,MAAM,CAAC;YAACvG,QAAQtC,EAAE8I,MAAM,GAAGC,IAAI;QAAE;QAC3CtC,MAAM9G;QACN+G,UAAU;YAAC,KAAK9H;QAAuC;IACzD;IACAgI,cAAcb;IACdc,SAAS,OAAO1E;QACd,MAAMsC,SAAS,MAAM7D,yBAAyB;YAC5CsE,SAAShD,eAAeC;YACxBG,QAAQH,QAAQ4C,MAAM,CAACzC,MAAM;YAC7B0G,QAAQ7G,QAAQsE,IAAI,CAACuC,MAAM;YAC3B/B,gBAAgB1E,sBAAsBJ;YACtCwC,eAAexC,QAAQgB,EAAE;QAC3B;QACA,OAAOqB,+BAA+BC;IACxC;AACF;AAEA,MAAMwE,sBAAsBlJ,YAAY;IACtCsG,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNzB,QAAQ/E,EAAE6I,MAAM,CAAC;YAACvG,QAAQtC,EAAE8I,MAAM,GAAGC,IAAI;QAAE;QAC3CtC,MAAMnH;QACNoH,UAAU;YAAC,KAAK9H;QAAuC;IACzD;IACAgI,cAAcb;IACdc,SAAS,OAAO1E;QACd,MAAMsC,SAAS,MAAMhE,4BAA4B;YAC/CyE,SAAShD,eAAeC;YACxBG,QAAQH,QAAQ4C,MAAM,CAACzC,MAAM;YAC7B,GAAIH,QAAQsE,IAAI,CAACuC,MAAM,GAAG;gBAACA,QAAQ7G,QAAQsE,IAAI,CAACuC,MAAM;YAAA,IAAI,CAAC,CAAC;YAC5D/B,gBAAgB1E,sBAAsBJ;YACtCwC,eAAexC,QAAQgB,EAAE;QAC3B;QACA,OAAOqB,+BAA+BC;IACxC;AACF;AAEA,MAAMyE,0BAA0BnJ,YAAY;IAC1CsG,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNzB,QAAQ/E,EAAE6I,MAAM,CAAC;YAACvG,QAAQtC,EAAE8I,MAAM,GAAGC,IAAI;QAAE;QAC3CtC,MAAMhH;QACNiH,UAAU;YAAC,KAAKhH;QAA6C;IAC/D;IACAkH,cAAcb;IACdc,SAAS,OAAO1E;QACd,MAAMsC,SAAS,MAAM9D,gCAAgC;YACnDuE,SAAShD,eAAeC;YACxBG,QAAQH,QAAQ4C,MAAM,CAACzC,MAAM;YAC7B,GAAIH,QAAQsE,IAAI,CAACuC,MAAM,GAAG;gBAACA,QAAQ7G,QAAQsE,IAAI,CAACuC,MAAM;YAAA,IAAI,CAAC,CAAC;YAC5D/B,gBAAgB1E,sBAAsBJ;YACtCwC,eAAexC,QAAQgB,EAAE;QAC3B;QACA,OAAO;YACL,GAAGqB,+BAA+BC,OAAO;YACzC0E,kBAAkB1E,OAAO2E,eAAe;QAC1C;IACF;AACF;AAEA,MAAMC,aAAatJ,YAAY;IAC7BsG,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNC,MAAMzH;QACN0H,UAAU;YAAC,KAAKzH;QAA4B;IAC9C;IACA2H,cAAcb;IACdc,SAAS,OAAO1E,SAAS2E;QACvB,MAAM5B,UAAUhD,eAAeC;QAC/B,MAAMe,QAAQ,MAAM7C,uBAAuB;YACzC6E;YACA5C,QAAQH,QAAQsE,IAAI,CAACrD,OAAO;YAC5BC,MAAMlB,QAAQsE,IAAI,CAACpD,IAAI;YACvB2F,QAAQ7G,QAAQsE,IAAI,CAACuC,MAAM;YAC3B/B,gBAAgB1E,sBAAsBJ;YACtCwC,eAAexC,QAAQgB,EAAE;QAC3B;QACA2D,MAAMI,IAAI,CAAC;QACX,OAAOjE,gBAAgBC;IACzB;AACF;AAEA,MAAMoG,cAAcvJ,YAAY;IAC9BsG,QAAQ;IACRC,MAAM;IACNC,aAAa;IACbC,QAAQ;QACNzB,QAAQ/E,EAAE6I,MAAM,CAAC;YAACtE,SAASvE,EAAE8I,MAAM,GAAGC,IAAI;QAAE;QAC5CtC,MAAMlH;QACNmH,UAAU;YAAC,KAAKlH;QAA8B;IAChD;IACAoH,cAAcb;IACdc,SAAS,OAAO1E;QACd,MAAMe,QAAQ,MAAMxC,yBAAyB;YAC3CwE,SAAShD,eAAeC;YACxBoC,SAASpC,QAAQ4C,MAAM,CAACR,OAAO;YAC/ByE,QAAQ7G,QAAQsE,IAAI,CAACuC,MAAM;YAC3B/B,gBAAgB1E,sBAAsBJ;YACtCwC,eAAexC,QAAQgB,EAAE;QAC3B;QACA,OAAOF,gBAAgBC;IACzB;AACF;AAEA,OAAO,MAAMqG,uBAAmCjL,8BAA8B;IAC5EkL,QAAQ;IACRC,MAAMpL;IACNqL,QAAQ;QAACtD;QAAgBiB;QAAWgC;QAAYC;KAAY;AAC9D,GAAG;AAEH,OAAO,MAAMK,gCAA4CrL,8BAA8B;IACrFkL,QAAQ;IACRC,MAAMpL;IACNqL,QAAQ;QAACvC;KAAoB;AAC/B,GAAG;AAEH,SAASyC,2BAA2BC,UAAuC;IACzE,OAAO9J,YAAY;QACjBsG,QAAQ;QACRC,MAAM;QACNC,aACE;QACFC,QAAQ;YACNzB,QAAQ/E,EAAE6I,MAAM,CAAC;gBAACvG,QAAQtC,EAAE8I,MAAM,GAAGC,IAAI;YAAE;YAC3CtC,MAAMtH;YACNuH,UAAU;gBAAC,KAAKxH;YAAyB;QAC3C;QACA,0EAA0E;QAC1E,iEAAiE;QACjE,sEAAsE;QACtE,2EAA2E;QAC3E,oEAAoE;QACpEyH,YAAY;YACV5E,mCAAmC;YACnC,CAACI;gBACC5D,2BAA2B4D;gBAC3B,OAAO2D;YACT;SACD;QACDc,cAAcb;QACdc,SAAS,OAAO1E;YACd,MAAMC,SAASP,iBAAiBM;YAChC,MAAMsC,SAAS,MAAMnE,gBAAgB;gBACnC4E,SAAShD,eAAeC;gBACxB,GAAIC,QAAQ0H,iBAAiB;oBAACC,qBAAqB3H,OAAO0H,cAAc;gBAAA,IAAI,CAAC,CAAC;gBAC9EE,cAAc7H,QAAQ4C,MAAM,CAACzC,MAAM;gBACnC0G,QAAQ7G,QAAQsE,IAAI,CAACuC,MAAM;gBAC3B/B,gBAAgB1E,sBAAsBJ;gBACtC,sEAAsE;gBACtE,qEAAqE;gBACrE,qEAAqE;gBACrE,sEAAsE;gBACtE,kBAAkB;gBAClBwC,eAAevG;gBACfyL;YACF;YACA,mEAAmE;YACnE,kEAAkE;YAClE,OAAO;gBACLI,OAAOxF,OAAOwF,KAAK;gBACnBC,YAAYzF,OAAO0F,SAAS,CAAC3G,WAAW;gBACxC4G,aAAa,IAAIC,OAAO7G,WAAW;gBACnC8G,iBAAiB7F,OAAOqF,cAAc;gBACtChG,MAAMhC,UAAU2C,OAAOX,IAAI;YAC7B;QACF;IACF;AACF;AAEA,OAAO,SAASyG,+BACdV,UAAuC;IAEvC,OAAO;QACLvL,8BAA8B;YAC5BkL,QAAQ;YACRC,MAAMpL;YACNqL,QAAQ;gBACNzB;gBACAF;gBACAa;gBACAK;gBACAC;aACD;QACH;QACA,yEAAyE;QACzE,kEAAkE;QAClE,sEAAsE;QACtE,yEAAyE;QACzE;YACEM,QAAQ;YACRC,MAAMpL;YACNqL,QAAQ;gBAACE,2BAA2BC;aAAY;QAClD;KACD;AACH"}
@@ -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"}
@@ -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;AAuC7B,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"}
@@ -41,6 +41,16 @@ const policies = {
41
41
  windowSeconds: 5 * 60
42
42
  }
43
43
  },
44
+ directory: {
45
+ ip: {
46
+ limit: 60,
47
+ windowSeconds: 5 * 60
48
+ },
49
+ actor: {
50
+ limit: 60,
51
+ windowSeconds: 5 * 60
52
+ }
53
+ },
44
54
  impersonate: {
45
55
  // Bounds mint and probing attempts: the IP bucket bounds aggregate source
46
56
  // traffic, and the actor bucket is a per-actor cap that prevents an actor
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/presentation/routes/rate-limit.ts"],"sourcesContent":["import {getUserContext} from '@shipfox/api-auth-context';\nimport {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';\nimport {enforceRateLimit as enforceSharedRateLimit} from '@shipfox/node-rate-limit';\nimport {\n type AuthRateLimitAction,\n type AuthRateLimitPolicy,\n type AuthRateLimitScope,\n checkAuthRateLimit,\n} from '#core/rate-limit.js';\n\nconst policies: Record<\n AuthRateLimitAction,\n Partial<Record<AuthRateLimitScope, AuthRateLimitPolicy>>\n> = {\n login: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n email: {limit: 10, windowSeconds: 15 * 60},\n },\n 'email-send': {\n ip: {limit: 30, windowSeconds: 60 * 60},\n email: {limit: 3, windowSeconds: 60 * 60},\n },\n bootstrap: {\n ip: {limit: 5, windowSeconds: 15 * 60},\n },\n 'bootstrap-state': {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n lookup: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n impersonate: {\n // Bounds mint and probing attempts: the IP bucket bounds aggregate source\n // traffic, and the actor bucket is a per-actor cap that prevents an actor\n // from evading the bound by rotating IPs (every request still consumes the\n // IP bucket first, so a shared NAT cannot exhaust it faster by pooling\n // actors). Ladder denials by an identifiable actor role are audited as\n // `failed` events.\n ip: {limit: 20, windowSeconds: 15 * 60},\n actor: {limit: 20, windowSeconds: 15 * 60},\n },\n};\n\ninterface EmailBody {\n email: string;\n}\n\nfunction routeName(request: FastifyRequest): string {\n return request.routeOptions.url ?? request.url.split('?')[0] ?? 'unknown';\n}\n\nasync function enforceRateLimit(params: {\n request: FastifyRequest;\n reply: FastifyReply;\n action: AuthRateLimitAction;\n scope: AuthRateLimitScope;\n identifier: string;\n}): Promise<void> {\n const policy = policies[params.action][params.scope];\n if (!policy) return;\n\n await enforceSharedRateLimit({\n request: params.request,\n reply: params.reply,\n check: () =>\n checkAuthRateLimit({\n action: params.action,\n scope: params.scope,\n identifier: params.identifier,\n ...policy,\n }),\n route: routeName(params.request),\n unavailableCode: 'auth-rate-limit-unavailable',\n unavailableMessage: 'Authentication rate limiter unavailable',\n setRetryAfter: (reply, retryAfterSeconds) => {\n reply.header('Retry-After', String(retryAfterSeconds));\n },\n logWarn: (request, context) => {\n request.log.warn(context, 'Auth rate limit blocked request');\n },\n logError: (request, context) => {\n request.log.error(context, 'Auth rate limiter unavailable');\n },\n createClientError: (presentation, cause) =>\n new ClientError(presentation.message, presentation.code, {\n status: presentation.status,\n ...(presentation.details ? {details: presentation.details} : {}),\n data: presentation.data,\n cause,\n }),\n });\n}\n\nexport function createAuthRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest<{Body: EmailBody}>, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'email',\n identifier: request.body.email,\n });\n };\n}\n\nexport function createAuthIpRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n };\n}\n\n/**\n * Rate limits an authenticated route by source IP and by actor. Every request\n * consumes the IP bucket, which bounds aggregate source traffic; the actor\n * bucket is an additional per-actor cap that prevents an actor from evading\n * the bound by rotating IPs. Runs after authentication (auth is an `onRequest`\n * hook), so the user context is set.\n */\nexport function createAuthActorRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n const client = getUserContext(request);\n if (client) {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'actor',\n identifier: client.userId,\n });\n }\n };\n}\n"],"names":["getUserContext","ClientError","enforceRateLimit","enforceSharedRateLimit","checkAuthRateLimit","policies","login","ip","limit","windowSeconds","email","bootstrap","lookup","impersonate","actor","routeName","request","routeOptions","url","split","params","policy","action","scope","reply","check","identifier","route","unavailableCode","unavailableMessage","setRetryAfter","retryAfterSeconds","header","String","logWarn","context","log","warn","logError","error","createClientError","presentation","cause","message","code","status","details","data","createAuthRateLimitPreHandler","body","createAuthIpRateLimitPreHandler","createAuthActorRateLimitPreHandler","client","userId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/presentation/routes/rate-limit.ts"],"sourcesContent":["import {getUserContext} from '@shipfox/api-auth-context';\nimport {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';\nimport {enforceRateLimit as enforceSharedRateLimit} from '@shipfox/node-rate-limit';\nimport {\n type AuthRateLimitAction,\n type AuthRateLimitPolicy,\n type AuthRateLimitScope,\n checkAuthRateLimit,\n} from '#core/rate-limit.js';\n\nconst policies: Record<\n AuthRateLimitAction,\n Partial<Record<AuthRateLimitScope, AuthRateLimitPolicy>>\n> = {\n login: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n email: {limit: 10, windowSeconds: 15 * 60},\n },\n 'email-send': {\n ip: {limit: 30, windowSeconds: 60 * 60},\n email: {limit: 3, windowSeconds: 60 * 60},\n },\n bootstrap: {\n ip: {limit: 5, windowSeconds: 15 * 60},\n },\n 'bootstrap-state': {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n lookup: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n directory: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n actor: {limit: 60, windowSeconds: 5 * 60},\n },\n impersonate: {\n // Bounds mint and probing attempts: the IP bucket bounds aggregate source\n // traffic, and the actor bucket is a per-actor cap that prevents an actor\n // from evading the bound by rotating IPs (every request still consumes the\n // IP bucket first, so a shared NAT cannot exhaust it faster by pooling\n // actors). Ladder denials by an identifiable actor role are audited as\n // `failed` events.\n ip: {limit: 20, windowSeconds: 15 * 60},\n actor: {limit: 20, windowSeconds: 15 * 60},\n },\n};\n\ninterface EmailBody {\n email: string;\n}\n\nfunction routeName(request: FastifyRequest): string {\n return request.routeOptions.url ?? request.url.split('?')[0] ?? 'unknown';\n}\n\nasync function enforceRateLimit(params: {\n request: FastifyRequest;\n reply: FastifyReply;\n action: AuthRateLimitAction;\n scope: AuthRateLimitScope;\n identifier: string;\n}): Promise<void> {\n const policy = policies[params.action][params.scope];\n if (!policy) return;\n\n await enforceSharedRateLimit({\n request: params.request,\n reply: params.reply,\n check: () =>\n checkAuthRateLimit({\n action: params.action,\n scope: params.scope,\n identifier: params.identifier,\n ...policy,\n }),\n route: routeName(params.request),\n unavailableCode: 'auth-rate-limit-unavailable',\n unavailableMessage: 'Authentication rate limiter unavailable',\n setRetryAfter: (reply, retryAfterSeconds) => {\n reply.header('Retry-After', String(retryAfterSeconds));\n },\n logWarn: (request, context) => {\n request.log.warn(context, 'Auth rate limit blocked request');\n },\n logError: (request, context) => {\n request.log.error(context, 'Auth rate limiter unavailable');\n },\n createClientError: (presentation, cause) =>\n new ClientError(presentation.message, presentation.code, {\n status: presentation.status,\n ...(presentation.details ? {details: presentation.details} : {}),\n data: presentation.data,\n cause,\n }),\n });\n}\n\nexport function createAuthRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest<{Body: EmailBody}>, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'email',\n identifier: request.body.email,\n });\n };\n}\n\nexport function createAuthIpRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n };\n}\n\n/**\n * Rate limits an authenticated route by source IP and by actor. Every request\n * consumes the IP bucket, which bounds aggregate source traffic; the actor\n * bucket is an additional per-actor cap that prevents an actor from evading\n * the bound by rotating IPs. Runs after authentication (auth is an `onRequest`\n * hook), so the user context is set.\n */\nexport function createAuthActorRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n const client = getUserContext(request);\n if (client) {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'actor',\n identifier: client.userId,\n });\n }\n };\n}\n"],"names":["getUserContext","ClientError","enforceRateLimit","enforceSharedRateLimit","checkAuthRateLimit","policies","login","ip","limit","windowSeconds","email","bootstrap","lookup","directory","actor","impersonate","routeName","request","routeOptions","url","split","params","policy","action","scope","reply","check","identifier","route","unavailableCode","unavailableMessage","setRetryAfter","retryAfterSeconds","header","String","logWarn","context","log","warn","logError","error","createClientError","presentation","cause","message","code","status","details","data","createAuthRateLimitPreHandler","body","createAuthIpRateLimitPreHandler","createAuthActorRateLimitPreHandler","client","userId"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"signup.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/registration/signup.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,2BAA2B,EAAC,MAAM,0CAA0C,CAAC;AAW1F,OAAO,EAAmC,KAAK,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAInF,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,2BAA2B,EACvC,YAAY,CAAC,EAAE,YAAY,mDAkG5B"}
1
+ {"version":3,"file":"signup.d.ts","sourceRoot":"","sources":["../../../../src/presentation/routes/registration/signup.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,2BAA2B,EAAC,MAAM,0CAA0C,CAAC;AAW1F,OAAO,EAAmC,KAAK,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAuDnF,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,2BAA2B,EACvC,YAAY,CAAC,EAAE,YAAY,mDA0D5B"}
@@ -6,6 +6,64 @@ import { EmailTakenError, InvitationEmailMismatchError, SignupNotAllowedError, T
6
6
  import { SIGNUP_DENIAL_MESSAGE_MAX_LENGTH } from '#core/ports.js';
7
7
  import { setRefreshTokenCookie } from '#presentation/auth/refresh-cookie.js';
8
8
  import { toUserDto } from '#presentation/dto/user.js';
9
+ function handleEmailChallengeError(error) {
10
+ if (!(error instanceof EmailChallengeError)) return;
11
+ throw new ClientError(error.message, `email-challenge-${error.code}`, {
12
+ status: error.code === 'limited' ? 429 : 400,
13
+ ...error.retryAt ? {
14
+ details: {
15
+ retry_at: error.retryAt.toISOString()
16
+ }
17
+ } : {}
18
+ });
19
+ }
20
+ function handleSignupPolicyError(error) {
21
+ if (error instanceof EmailTakenError) {
22
+ throw new ClientError('Email already registered', 'email-taken', {
23
+ status: 409
24
+ });
25
+ }
26
+ if (error instanceof SignupNotAllowedError) {
27
+ const message = error.message.slice(0, SIGNUP_DENIAL_MESSAGE_MAX_LENGTH);
28
+ throw new ClientError(message, 'signup-not-allowed', {
29
+ status: 403,
30
+ details: {
31
+ message,
32
+ ...error.format ? {
33
+ format: error.format
34
+ } : {}
35
+ }
36
+ });
37
+ }
38
+ }
39
+ function handleInvitationError(error) {
40
+ if (error instanceof TokenInvalidError) {
41
+ throw new ClientError('Invitation token is invalid', 'invitation-token-invalid', {
42
+ status: 410
43
+ });
44
+ }
45
+ if (error instanceof TokenAlreadyUsedError) {
46
+ throw new ClientError('Invitation has already been accepted', 'invitation-token-used', {
47
+ status: 410
48
+ });
49
+ }
50
+ if (error instanceof TokenExpiredError) {
51
+ throw new ClientError('Invitation has expired', 'invitation-token-expired', {
52
+ status: 410
53
+ });
54
+ }
55
+ if (error instanceof InvitationEmailMismatchError) {
56
+ throw new ClientError('Signup email does not match the invitation', 'invitation-email-mismatch', {
57
+ status: 403
58
+ });
59
+ }
60
+ }
61
+ function handleSignupError(error) {
62
+ handleEmailChallengeError(error);
63
+ handleSignupPolicyError(error);
64
+ handleInvitationError(error);
65
+ throw error;
66
+ }
9
67
  export function createSignupRoute(workspaces, signupPolicy) {
10
68
  return defineRoute({
11
69
  method: 'POST',
@@ -17,56 +75,7 @@ export function createSignupRoute(workspaces, signupPolicy) {
17
75
  201: signupResponseSchema
18
76
  }
19
77
  },
20
- errorHandler: (error)=>{
21
- if (error instanceof EmailChallengeError) {
22
- throw new ClientError(error.message, `email-challenge-${error.code}`, {
23
- status: error.code === 'limited' ? 429 : 400,
24
- ...error.retryAt ? {
25
- details: {
26
- retry_at: error.retryAt.toISOString()
27
- }
28
- } : {}
29
- });
30
- }
31
- if (error instanceof EmailTakenError) {
32
- throw new ClientError('Email already registered', 'email-taken', {
33
- status: 409
34
- });
35
- }
36
- if (error instanceof SignupNotAllowedError) {
37
- const message = error.message.slice(0, SIGNUP_DENIAL_MESSAGE_MAX_LENGTH);
38
- throw new ClientError(message, 'signup-not-allowed', {
39
- status: 403,
40
- details: {
41
- message,
42
- ...error.format ? {
43
- format: error.format
44
- } : {}
45
- }
46
- });
47
- }
48
- if (error instanceof TokenInvalidError) {
49
- throw new ClientError('Invitation token is invalid', 'invitation-token-invalid', {
50
- status: 410
51
- });
52
- }
53
- if (error instanceof TokenAlreadyUsedError) {
54
- throw new ClientError('Invitation has already been accepted', 'invitation-token-used', {
55
- status: 410
56
- });
57
- }
58
- if (error instanceof TokenExpiredError) {
59
- throw new ClientError('Invitation has expired', 'invitation-token-expired', {
60
- status: 410
61
- });
62
- }
63
- if (error instanceof InvitationEmailMismatchError) {
64
- throw new ClientError('Signup email does not match the invitation', 'invitation-email-mismatch', {
65
- status: 403
66
- });
67
- }
68
- throw error;
69
- },
78
+ errorHandler: handleSignupError,
70
79
  handler: async (request, reply)=>{
71
80
  const { email, password, name, invitation_token } = request.body;
72
81
  if (invitation_token === undefined) {